summaryrefslogtreecommitdiff
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
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
-rw-r--r--LICENSE2
-rw-r--r--Makefile13
-rw-r--r--README.rst2
-rw-r--r--bottle.py208
-rwxr-xr-xdocs/api.rst29
-rwxr-xr-xdocs/changelog.rst8
-rw-r--r--docs/deployment.rst7
-rwxr-xr-xdocs/index.rst4
-rwxr-xr-xdocs/plugindev.rst2
-rwxr-xr-xdocs/tutorial.rst6
-rw-r--r--docs/tutorial_app.rst22
-rwxr-xr-xtest/test_environ.py36
-rwxr-xr-xtest/test_outputfilter.py18
-rwxr-xr-xtest/test_sendfile.py18
-rwxr-xr-xtest/test_wsgi.py6
15 files changed, 196 insertions, 185 deletions
diff --git a/LICENSE b/LICENSE
index fb43a45..cdd0c70 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011, Marcel Hellkamp.
+Copyright (c) 2012, Marcel Hellkamp.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index 76b9a87..fdb9e72 100644
--- a/Makefile
+++ b/Makefile
@@ -2,14 +2,18 @@ PATH := build/python/bin:$(PATH)
VERSION = $(shell python setup.py --version)
ALLFILES = $(shell echo bottle.py test/*.py test/views/*.tpl)
-.PHONY: release install docs test test_all test_25 test_26 test_27 test_31 test_32 2to3 clean
+.PHONY: release install docs test test_all test_25 test_26 test_27 test_31 test_32 test_33 2to3 clean
release: test_all
python setup.py --version | egrep -q -v '[a-zA-Z]' # Fail on dev/rc versions
git commit -e -m "Release of $(VERSION)" # Fail on nothing to commit
git tag -a -m "Release of $(VERSION)" $(VERSION) # Fail on existing tags
+ git push origin HEAD # Fail on out-of-sync upstream
+ git push origin tag $(VERSION) # Fail on dublicate tag
python setup.py sdist register upload # Release to pypi
- echo "Do not forget to: git push --tags"
+
+push: test_all
+ git push origin HEAD
install:
python setup.py install
@@ -21,7 +25,7 @@ test:
which python
python test/testall.py
-test_all: test_25 test_26 test_27 test_31 test_32
+test_all: test_25 test_26 test_27 test_31 test_32 test_33
test_25:
python2.5 test/testall.py
@@ -38,6 +42,9 @@ test_31:
test_32:
python3.2 test/testall.py
+test_33:
+ python3.3 test/testall.py
+
clean:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
diff --git a/README.rst b/README.rst
index 1d3e080..4854e37 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
Bottle Web Framework
====================
-.. image:: http://bottlepy.org/bottle-logo.png
+.. image:: http://bottlepy.org/docs/dev/_static/logo_nav.png
:alt: Bottle Logo
:align: right
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
diff --git a/docs/api.rst b/docs/api.rst
index f2c8129..aeb4ff0 100755
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -107,16 +107,6 @@ Exceptions
.. autoexception:: BottleException
:members:
-.. autoexception:: HTTPResponse
- :members:
-
-.. autoexception:: HTTPError
- :members:
-
-.. autoexception:: RouteReset
- :members:
-
-
The :class:`Bottle` Class
@@ -134,18 +124,16 @@ The :class:`Request` Object
The :class:`Request` class wraps a WSGI environment and provides helpful methods to parse and access form data, cookies, file uploads and other metadata. Most of the attributes are read-only.
-You usually don't instantiate :class:`Request` yourself, but use the module-level :data:`bottle.request` instance. This instance is thread-local and refers to the `current` request, or in other words, the request that is currently processed by the request handler in the current context. `Thread locality` means that you can safely use a global instance in a multithreaded environment.
-
.. autoclass:: Request
:members:
-.. autoclass:: LocalRequest
- :members:
-
.. autoclass:: BaseRequest
:members:
+The module-level :data:`bottle.request` is a proxy object (implemented in :class:`LocalRequest`) and always refers to the `current` request, or in other words, the request that is currently processed by the request handler in the current thread. This `thread locality` ensures that you can safely use a global instance in a multi-threaded environment.
+.. autoclass:: LocalRequest
+ :members:
The :class:`Response` Object
@@ -156,10 +144,19 @@ The :class:`Response` class stores the HTTP status code as well as headers and c
.. autoclass:: Response
:members:
+.. autoclass:: BaseResponse
+ :members:
+
.. autoclass:: LocalResponse
:members:
-.. autoclass:: BaseResponse
+
+The following two classes can be raised as an exception. The most noticeable difference is that bottle invokes error handlers for :class:`HTTPError`, but not for :class:`HTTPResponse` or other response types.
+
+.. autoexception:: HTTPResponse
+ :members:
+
+.. autoexception:: HTTPError
:members:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index cda94fd..44a37f4 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -12,10 +12,10 @@ Release 0.11
* Native support for Python 2.x and 3.x syntax. No need to run 2to3 anymore.
* Support for partial downloads (``Range`` header) in :func:`static_file`.
-* The new :class:`ResourceManager` interface helps locating files bundled with the application.
+* The new :class:`ResourceManager` interface helps locating files bundled with an application.
* Added a server adapter for `waitress <http://docs.pylonsproject.org/projects/waitress/en/latest/>`_.
* New :meth:`Bottle.merge` method to install all routes from one application into another.
-* New :attr:`BaseRequest.app` property to get the application object that handles that request.
+* New :attr:`BaseRequest.app` property to get the application object that handles a request.
* Added :meth:`FormsDict.decode()` to get an all-unicode version (needed by WTForms).
* :class:`MultiDict` and subclasses are now pickle-able.
@@ -95,7 +95,7 @@ This release is mostly backward compatible, but some APIs are marked deprecated
Release 0.8
============
-.. rubric:: API changes
+.. rubric:: API changes
These changes may break compatibility with previous versions.
@@ -121,7 +121,7 @@ These changes may break compatibility with previous versions.
.. rubric:: New features
-This is an incomplete list of new features and improved functionality.
+This is an incomplete list of new features and improved functionality.
* The :class:`Request` object got new properties: :attr:`Request.body`, :attr:`Request.auth`, :attr:`Request.url`, :attr:`Request.header`, :attr:`Request.forms`, :attr:`Request.files`.
* The :meth:`Response.set_cookie` and :meth:`Request.get_cookie` methods are now able to encode and decode python objects. This is called a *secure cookie* because the encoded values are signed and protected from changes on client side. All pickle-able data structures are allowed.
diff --git a/docs/deployment.rst b/docs/deployment.rst
index d97fcb1..7371ac2 100644
--- a/docs/deployment.rst
+++ b/docs/deployment.rst
@@ -14,6 +14,11 @@
.. _gevent: http://www.gevent.org/
.. _eventlet: http://eventlet.net/
.. _waitress: http://readthedocs.org/docs/waitress/en/latest/
+.. _apache: http://httpd.apache.org/
+.. _mod_wsgi: http://code.google.com/p/modwsgi/
+.. _pound: http://www.apsis.ch/pound
+
+
.. _tutorial-deployment:
@@ -87,7 +92,7 @@ If there is no adapter for your favorite server or if you need more control over
Apache mod_wsgi
--------------------------------------------------------------------------------
-Instead of running your own HTTP server from within Bottle, you can attach Bottle applications to an `Apache server`_ using mod_wsgi_.
+Instead of running your own HTTP server from within Bottle, you can attach Bottle applications to an `Apache server <apache>`_ using mod_wsgi_.
All you need is an ``app.wsgi`` file that provides an ``application`` object. This object is used by mod_wsgi to start your application and should be a WSGI-compatible Python callable.
diff --git a/docs/index.rst b/docs/index.rst
index 9179c4a..1bb081b 100755
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -31,11 +31,11 @@ Bottle is a fast, simple and lightweight WSGI_ micro web-framework for Python_.
::
- from bottle import route, run
+ from bottle import route, run, template
@route('/hello/:name')
def index(name='World'):
- return '<b>Hello %s!</b>' % name
+ return template('<b>Hello {{name}}</b>!', name=name)
run(host='localhost', port=8080)
diff --git a/docs/plugindev.rst b/docs/plugindev.rst
index 1649149..40da98c 100755
--- a/docs/plugindev.rst
+++ b/docs/plugindev.rst
@@ -31,7 +31,7 @@ Of course, this is just a simplification. Plugins can do a lot more than just de
return body
return wrapper
- bottle.install(stopwatch)
+ install(stopwatch)
This plugin measures the execution time for each request and adds an appropriate ``X-Exec-Time`` header to the response. As you can see, the plugin returns a wrapper and the wrapper calls the original callback recursively. This is how decorators usually work.
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 9c1bccb..9ef531d 100755
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -23,7 +23,7 @@
Tutorial
========
-This tutorial introduces you to the concepts and features of the Bottle web framework and covers basic and advanced topics alike. You can read it from start to end, or use it as a refecence later on. The automatically generated :doc:`api` may be interesting for you, too. It covers more details, but explains less than this tutorial. Solutions for the most common questions can be found in our :doc:`recipes` collection or on the :doc:`faq` page. If you need any help, join our `mailing list <mailto:bottlepy@googlegroups.com>`_ or visit us in our `IRC channel <http://webchat.freenode.net/?channels=bottlepy>`_.
+This tutorial introduces you to the concepts and features of the Bottle web framework and covers basic and advanced topics alike. You can read it from start to end, or use it as a reference later on. The automatically generated :doc:`api` may be interesting for you, too. It covers more details, but explains less than this tutorial. Solutions for the most common questions can be found in our :doc:`recipes` collection or on the :doc:`faq` page. If you need any help, join our `mailing list <mailto:bottlepy@googlegroups.com>`_ or visit us in our `IRC channel <http://webchat.freenode.net/?channels=bottlepy>`_.
.. _installation:
@@ -93,7 +93,7 @@ The `Default Application`
For the sake of simplicity, most examples in this tutorial use a module-level :func:`route` decorator to define routes. This adds routes to a global "default application", an instance of :class:`Bottle` that is automatically created the first time you call :func:`route`. Several other module-level decorators and functions relate to this default application, but if you prefer a more object oriented approach and don't mind the extra typing, you can create a separate application object and use that instead of the global one::
- from bottle import Bottle, run
+ from bottle import Bottle, run, template
app = Bottle()
@@ -124,7 +124,7 @@ The :func:`route` decorator links an URL path to a callback function, and adds a
@route('/')
@route('/hello/<name>')
def greet(name='Stranger'):
- return 'Hello %s, how are you?' % name
+ return template('Hello {{name}}, how are you?', name=name)
This example demonstrates two things: You can bind more than one route to a single callback, and you can add wildcards to URLs and access them via keyword arguments.
diff --git a/docs/tutorial_app.rst b/docs/tutorial_app.rst
index 0a0201d..0b41ee0 100644
--- a/docs/tutorial_app.rst
+++ b/docs/tutorial_app.rst
@@ -250,21 +250,21 @@ The code needs to be extended to::
@route('/new', method='GET')
def new_item():
- if request.GET.get('save','').strip():
+ if request.GET.get('save','').strip():
- new = request.GET.get('task', '').strip()
- conn = sqlite3.connect('todo.db')
- c = conn.cursor()
+ new = request.GET.get('task', '').strip()
+ conn = sqlite3.connect('todo.db')
+ c = conn.cursor()
- c.execute("INSERT INTO todo (task,status) VALUES (?,?)", (new,1))
- new_id = c.lastrowid
+ c.execute("INSERT INTO todo (task,status) VALUES (?,?)", (new,1))
+ new_id = c.lastrowid
- conn.commit()
- c.close()
+ conn.commit()
+ c.close()
- return '<p>The new task was inserted into the database, the ID is %s</p>' % new_id
- else:
- return template('new_task.tpl')
+ return '<p>The new task was inserted into the database, the ID is %s</p>' % new_id
+ else:
+ return template('new_task.tpl')
``new_task.tpl`` looks like this::
diff --git a/test/test_environ.py b/test/test_environ.py
index b761f48..434d168 100755
--- a/test/test_environ.py
+++ b/test/test_environ.py
@@ -74,7 +74,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(['/', '/a/b/c/d/'], test_shift('/a/b/c/d', '/', -4))
self.assertRaises(AssertionError, test_shift, '/a/b', '/c/d', 3)
self.assertRaises(AssertionError, test_shift, '/a/b', '/c/d', -3)
-
+
def test_url(self):
""" Environ: URL building """
request = BaseRequest({'HTTP_HOST':'example.com'})
@@ -122,7 +122,7 @@ class TestRequest(unittest.TestCase):
self.assertTrue('Some-Header' in request.headers)
self.assertTrue(request.headers['Some-Header'] == 'some value')
self.assertTrue(request.headers['Some-Other-Header'] == 'some other value')
-
+
def test_header_access_special(self):
e = {}
wsgiref.util.setup_testing_defaults(e)
@@ -133,7 +133,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(request.headers['Content-Length'], '123')
def test_cookie_dict(self):
- """ Environ: Cookie dict """
+ """ Environ: Cookie dict """
t = dict()
t['a=a'] = {'a': 'a'}
t['a=a; b=b'] = {'a': 'a', 'b':'b'}
@@ -145,7 +145,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(v[n], request.get_cookie(n))
def test_get(self):
- """ Environ: GET data """
+ """ Environ: GET data """
qs = tonat(tob('a=a&a=1&b=b&c=c&cn=%e7%93%b6'), 'latin1')
request = BaseRequest({'QUERY_STRING':qs})
self.assertTrue('a' in request.query)
@@ -156,9 +156,9 @@ class TestRequest(unittest.TestCase):
self.assertEqual('b', request.query['b'])
self.assertEqual(tonat(tob('瓶'), 'latin1'), request.query['cn'])
self.assertEqual(touni('瓶'), request.query.cn)
-
+
def test_post(self):
- """ Environ: POST data """
+ """ Environ: POST data """
sq = tob('a=a&a=1&b=b&c=&d&cn=%e7%93%b6')
e = {}
wsgiref.util.setup_testing_defaults(e)
@@ -204,7 +204,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(sq, request.body.read())
def test_params(self):
- """ Environ: GET and POST are combined in request.param """
+ """ Environ: GET and POST are combined in request.param """
e = {}
wsgiref.util.setup_testing_defaults(e)
e['wsgi.input'].write(tob('b=b&c=p'))
@@ -217,7 +217,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual('p', request.params['c'])
def test_getpostleak(self):
- """ Environ: GET and POST should not leak into each other """
+ """ Environ: GET and POST should not leak into each other """
e = {}
wsgiref.util.setup_testing_defaults(e)
e['wsgi.input'].write(tob('b=b'))
@@ -230,7 +230,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(['b'], list(request.POST.keys()))
def test_body(self):
- """ Environ: Request.body should behave like a file object factory """
+ """ Environ: Request.body should behave like a file object factory """
e = {}
wsgiref.util.setup_testing_defaults(e)
e['wsgi.input'].write(tob('abc'))
@@ -250,7 +250,7 @@ class TestRequest(unittest.TestCase):
e['wsgi.input'].seek(0)
e['CONTENT_LENGTH'] = str(1024*1000)
request = BaseRequest(e)
- self.assertTrue(hasattr(request.body, 'fileno'))
+ self.assertTrue(hasattr(request.body, 'fileno'))
self.assertEqual(1024*1000, len(request.body.read()))
self.assertEqual(1024, len(request.body.read(1024)))
self.assertEqual(1024*1000, len(request.body.readline()))
@@ -492,7 +492,7 @@ class TestResponse(unittest.TestCase):
def test_content_type(self):
rs = BaseResponse()
rs.content_type = 'test/some'
- self.assertEquals('test/some', rs.headers.get('Content-Type'))
+ self.assertEquals('test/some', rs.headers.get('Content-Type'))
def test_charset(self):
rs = BaseResponse()
@@ -553,7 +553,7 @@ class TestResponse(unittest.TestCase):
if name.title() == 'X-Test']
self.assertEqual(['bar'], headers)
self.assertEqual('bar', response['x-test'])
-
+
def test_append_header(self):
response = BaseResponse()
response.set_header('x-test', 'foo')
@@ -562,7 +562,7 @@ class TestResponse(unittest.TestCase):
self.assertEqual(['foo'], headers)
self.assertEqual('foo', response['x-test'])
- response.set_header('X-Test', 'bar', True)
+ response.add_header('X-Test', 'bar')
headers = [value for name, value in response.headerlist
if name.title() == 'X-Test']
self.assertEqual(['foo', 'bar'], headers)
@@ -585,10 +585,10 @@ class TestResponse(unittest.TestCase):
class TestRedirect(unittest.TestCase):
-
+
def assertRedirect(self, target, result, query=None, status=303, **args):
env = {'SERVER_PROTOCOL':'HTTP/1.1'}
- for key in args:
+ for key in list(args):
if key.startswith('wsgi'):
args[key.replace('_', '.', 1)] = args[key]
del args[key]
@@ -598,7 +598,7 @@ class TestRedirect(unittest.TestCase):
bottle.redirect(target, **(query or {}))
except bottle.HTTPResponse:
r = _e()
- self.assertEqual(status, r.status)
+ self.assertEqual(status, r.status_code)
self.assertTrue(r.headers)
self.assertEqual(result, r.headers['Location'])
@@ -641,7 +641,7 @@ class TestRedirect(unittest.TestCase):
SCRIPT_NAME='/foo/', PATH_INFO='/bar/baz.html')
self.assertRedirect('../baz/../test.html', 'http://127.0.0.1/foo/test.html',
PATH_INFO='/foo/bar/')
-
+
def test_sheme(self):
self.assertRedirect('./test.html', 'https://127.0.0.1/test.html',
wsgi_url_scheme='https')
@@ -679,7 +679,7 @@ class TestRedirect(unittest.TestCase):
self.assertRedirect('./te st.html',
'http://example.com/a%20a/b%20b/te st.html',
HTTP_HOST='example.com', SCRIPT_NAME='/a a/', PATH_INFO='/b b/')
-
+
class TestWSGIHeaderDict(unittest.TestCase):
def setUp(self):
self.env = {}
diff --git a/test/test_outputfilter.py b/test/test_outputfilter.py
index fb282cf..48b15f0 100755
--- a/test/test_outputfilter.py
+++ b/test/test_outputfilter.py
@@ -94,7 +94,7 @@ class TestOutputFilter(ServerTestBase):
yield 'foo'
self.assertBody('foo')
self.assertHeader('Test-Header', 'test')
-
+
def test_empty_generator_callback(self):
@self.app.route('/')
def test():
@@ -102,7 +102,7 @@ class TestOutputFilter(ServerTestBase):
bottle.response.headers['Test-Header'] = 'test'
self.assertBody('')
self.assertHeader('Test-Header', 'test')
-
+
def test_error_in_generator_callback(self):
@self.app.route('/')
def test():
@@ -113,7 +113,7 @@ class TestOutputFilter(ServerTestBase):
def test_fatal_error_in_generator_callback(self):
@self.app.route('/')
def test():
- yield
+ yield
raise KeyboardInterrupt()
self.assertRaises(KeyboardInterrupt, self.assertStatus, 500)
@@ -123,28 +123,28 @@ class TestOutputFilter(ServerTestBase):
yield
bottle.abort(404, 'teststring')
self.assertInBody('teststring')
- self.assertInBody('Error 404: Not Found')
+ self.assertInBody('404 Not Found')
self.assertStatus(404)
def test_httpresponse_in_generator_callback(self):
@self.app.route('/')
def test():
yield bottle.HTTPResponse('test')
- self.assertBody('test')
-
+ self.assertBody('test')
+
def test_unicode_generator_callback(self):
@self.app.route('/')
def test():
yield touni('äöüß')
- self.assertBody(touni('äöüß').encode('utf8'))
-
+ self.assertBody(touni('äöüß').encode('utf8'))
+
def test_invalid_generator_callback(self):
@self.app.route('/')
def test():
yield 1234
self.assertStatus(500)
self.assertInBody('Unsupported response type')
-
+
def test_cookie(self):
""" WSGI: Cookies """
@bottle.route('/cookie')
diff --git a/test/test_sendfile.py b/test/test_sendfile.py
index bf951f5..9d708e4 100755
--- a/test/test_sendfile.py
+++ b/test/test_sendfile.py
@@ -42,17 +42,17 @@ class TestSendFile(unittest.TestCase):
def test_valid(self):
""" SendFile: Valid requests"""
out = static_file(os.path.basename(__file__), root='./')
- self.assertEqual(open(__file__,'rb').read(), out.output.read())
+ self.assertEqual(open(__file__,'rb').read(), out.body.read())
def test_invalid(self):
""" SendFile: Invalid requests"""
- self.assertEqual(404, static_file('not/a/file', root='./').status)
+ self.assertEqual(404, static_file('not/a/file', root='./').status_code)
f = static_file(os.path.join('./../', os.path.basename(__file__)), root='./views/')
- self.assertEqual(403, f.status)
+ self.assertEqual(403, f.status_code)
try:
fp, fn = tempfile.mkstemp()
os.chmod(fn, 0)
- self.assertEqual(403, static_file(fn, root='/').status)
+ self.assertEqual(403, static_file(fn, root='/').status_code)
finally:
os.close(fp)
os.unlink(fn)
@@ -70,7 +70,7 @@ class TestSendFile(unittest.TestCase):
request.environ['HTTP_IF_MODIFIED_SINCE'] = \
time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
res = static_file(os.path.basename(__file__), root='./')
- self.assertEqual(304, res.status)
+ self.assertEqual(304, res.status_code)
self.assertEqual(int(os.stat(__file__).st_mtime),
parse_date(res.headers['Last-Modified']))
self.assertAlmostEqual(int(time.time()),
@@ -78,7 +78,7 @@ class TestSendFile(unittest.TestCase):
with self.context:
request.environ['HTTP_IF_MODIFIED_SINCE'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(100))
res = static_file(os.path.basename(__file__), root='./')
- self.assertEqual(open(__file__,'rb').read(), res.output.read())
+ self.assertEqual(open(__file__,'rb').read(), res.body.read())
def test_download(self):
""" SendFile: Download as attachment """
@@ -89,7 +89,7 @@ class TestSendFile(unittest.TestCase):
request.environ['HTTP_IF_MODIFIED_SINCE'] =\
time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(100))
f = static_file(os.path.basename(__file__), root='./')
- self.assertEqual(open(__file__,'rb').read(), f.output.read())
+ self.assertEqual(open(__file__,'rb').read(), f.body.read())
def test_range(self):
basename = os.path.basename(__file__)
@@ -97,11 +97,11 @@ class TestSendFile(unittest.TestCase):
request.environ['HTTP_RANGE'] = 'bytes=10-25,-80'
f = static_file(basename, root='./')
c = open(basename, 'rb'); c.seek(10)
- self.assertEqual(c.read(16), tob('').join(f.output))
+ self.assertEqual(c.read(16), tob('').join(f.body))
self.assertEqual('bytes 10-25/%d' % len(open(basename, 'rb').read()),
f.headers['Content-Range'])
self.assertEqual('bytes', f.headers['Accept-Ranges'])
-
+
def test_range_parser(self):
r = lambda rs: list(parse_range_header(rs, 100))
self.assertEqual([(90, 100)], r('bytes=-10'))
diff --git a/test/test_wsgi.py b/test/test_wsgi.py
index 318e0ca..8c451b8 100755
--- a/test/test_wsgi.py
+++ b/test/test_wsgi.py
@@ -92,12 +92,12 @@ class TestWsgi(ServerTestBase):
""" WSGI: abort(401, '') (HTTP 401) """
@bottle.route('/')
def test(): bottle.abort(401)
- self.assertStatus(401,'/')
+ self.assertStatus(401, '/')
@bottle.error(401)
def err(e):
bottle.response.status = 200
return str(type(e))
- self.assertStatus(200,'/')
+ self.assertStatus(200, '/')
self.assertBody("<class 'bottle.HTTPError'>",'/')
def test_303(self):
@@ -281,7 +281,7 @@ class TestDecorators(ServerTestBase):
def test():
return bottle.HTTPError(401, 'The cake is a lie!')
self.assertInBody('The cake is a lie!', '/tpl')
- self.assertInBody('401: Unauthorized', '/tpl')
+ self.assertInBody('401 Unauthorized', '/tpl')
self.assertStatus(401, '/tpl')
def test_truncate_body(self):