summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2012-11-21 15:49:33 +0100
committerMarcel Hellkamp <marc@gsites.de>2012-11-21 15:49:33 +0100
commit7b5aac96fc57b5016ba159085f9ff982a4b3afe5 (patch)
treee0c0a879283b0973020fe83719842b609588db21
parent5b86236bce8f8395888d2e3581ee42481ea18499 (diff)
parent97184f651205b01bcec78d2ff5f139116042fb02 (diff)
downloadbottle-7b5aac96fc57b5016ba159085f9ff982a4b3afe5.tar.gz
Merge branch 'release-0.11' of github.com:defnull/bottle into release-0.11
-rw-r--r--LICENSE2
-rw-r--r--Makefile13
-rw-r--r--bottle.py211
-rwxr-xr-xdocs/api.rst29
-rwxr-xr-xdocs/changelog.rst2
-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.py38
-rwxr-xr-xtest/test_outputfilter.py18
-rwxr-xr-xtest/test_sendfile.py20
-rwxr-xr-xtest/test_wsgi.py6
14 files changed, 198 insertions, 182 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 7ba4722..40b5384 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
@@ -20,7 +24,7 @@ docs:
test:
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
@@ -37,6 +41,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/bottle.py b/bottle.py
index 31519f6..e628f3e 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.11.3'
__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,13 +748,17 @@ 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
request.bind(environ)
response.bind()
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:
@@ -807,7 +783,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))\
@@ -818,19 +795,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'):
@@ -872,12 +848,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
@@ -1310,8 +1284,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
@@ -1354,7 +1326,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
@@ -1371,7 +1343,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
@@ -1385,13 +1357,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. '''
@@ -1400,16 +1369,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
@@ -1418,7 +1378,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)
@@ -1547,9 +1516,37 @@ class LocalResponse(BaseResponse):
_headers = local_property('response_headers')
body = local_property('response_body')
-Response = LocalResponse # BC 0.9
-Request = LocalRequest # BC 0.9
+Request = BaseRequest
+Response = BaseResponse
+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)
@@ -1568,7 +1565,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):
@@ -1618,7 +1615,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')
@@ -1840,7 +1837,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):
@@ -2047,7 +2044,10 @@ 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))
+ res = HTTPResponse("", status=code, Location=location)
+ if response._cookies:
+ res._cookies = response._cookies
+ raise res
def _file_iter_range(fp, offset, bytes, maxread=1024*1024):
@@ -2068,7 +2068,7 @@ 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()
+ headers = dict()
if not filename.startswith(root):
return HTTPError(403, "Access denied.")
@@ -2079,41 +2079,41 @@ 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("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
- header['Last-Modified'] = lm
+ headers['Last-Modified'] = lm
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("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
- return HTTPResponse(status=304, header=header)
+ headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
+ return HTTPResponse(status=304, **headers)
body = '' if request.method == 'HEAD' else open(filename, 'rb')
- header["Accept-Ranges"] = "bytes"
+ headers["Accept-Ranges"] = "bytes"
ranges = request.environ.get('HTTP_RANGE')
if 'HTTP_RANGE' in request.environ:
ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen))
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, status=206, **headers)
+ return HTTPResponse(body, **headers)
@@ -2486,15 +2486,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()
@@ -2801,11 +2801,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)
@@ -3166,11 +3174,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;
@@ -3179,10 +3186,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>
@@ -3218,7 +3225,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 dbe387f..44a37f4 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -12,7 +12,7 @@ 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 a request.
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 aabd929..930280c 100755
--- a/test/test_environ.py
+++ b/test/test_environ.py
@@ -73,7 +73,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'})
@@ -121,7 +121,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)
@@ -132,7 +132,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'}
@@ -144,7 +144,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)
@@ -155,9 +155,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)
@@ -203,7 +203,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'))
@@ -216,7 +216,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'))
@@ -229,7 +229,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'))
@@ -249,7 +249,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()))
@@ -490,7 +490,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()
@@ -551,7 +551,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')
@@ -560,7 +560,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)
@@ -583,10 +583,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]
@@ -596,10 +596,10 @@ 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'])
-
+
def test_absolute_path(self):
self.assertRedirect('/', 'http://127.0.0.1/')
self.assertRedirect('/test.html', 'http://127.0.0.1/test.html')
@@ -639,7 +639,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')
@@ -677,7 +677,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 c7907ee..502a499 100755
--- a/test/test_sendfile.py
+++ b/test/test_sendfile.py
@@ -41,21 +41,21 @@ 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)
-
+
def test_mime(self):
""" SendFile: Mime Guessing"""
f = static_file(os.path.basename(__file__), root='./')
@@ -67,11 +67,11 @@ class TestSendFile(unittest.TestCase):
""" SendFile: If-Modified-Since"""
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()), parse_date(res.headers['Date']))
request.environ['HTTP_IF_MODIFIED_SINCE'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(100))
- self.assertEqual(open(__file__,'rb').read(), static_file(os.path.basename(__file__), root='./').output.read())
+ self.assertEqual(open(__file__,'rb').read(), static_file(os.path.basename(__file__), root='./').body.read())
def test_download(self):
""" SendFile: Download as attachment """
@@ -80,18 +80,18 @@ class TestSendFile(unittest.TestCase):
self.assertEqual('attachment; filename="%s"' % basename, f.headers['Content-Disposition'])
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__)
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 ce2612f..5ef9f79 100755
--- a/test/test_wsgi.py
+++ b/test/test_wsgi.py
@@ -90,12 +90,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):
@@ -278,7 +278,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):