summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2012-08-17 00:23:43 +0200
committerMarcel Hellkamp <marc@gsites.de>2012-08-30 01:41:42 +0200
commit49c65935f554c708797300a57ef26c1b0d621c29 (patch)
tree506e053daf84fc7395e9c5330751957ad37e0bd0
parent4047cda0ab5aaab2472888c3fe0863edf4bd9bc6 (diff)
downloadbottle-49c65935f554c708797300a57ef26c1b0d621c29.tar.gz
Replaced HTTPResponse and HTTPError with subclasses of BaseResponse.
-rw-r--r--bottle.py72
-rwxr-xr-xdocs/api.rst29
-rwxr-xr-xtest/test_environ.py34
-rwxr-xr-xtest/test_outputfilter.py18
-rwxr-xr-xtest/test_sendfile.py20
-rwxr-xr-xtest/test_wsgi.py6
6 files changed, 88 insertions, 91 deletions
diff --git a/bottle.py b/bottle.py
index 6fe54a1..bdafbcf 100644
--- a/bottle.py
+++ b/bottle.py
@@ -16,7 +16,7 @@ License: MIT (see LICENSE for details)
from __future__ import with_statement
__author__ = 'Marcel Hellkamp'
-__version__ = '0.11.dev'
+__version__ = '0.11.rc1'
__license__ = 'MIT'
# The gevent server adapter needs to patch some modules before they are imported
@@ -211,34 +211,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))
-
-
@@ -776,6 +748,9 @@ 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
@@ -824,7 +799,7 @@ class Bottle(object):
# 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)
+ out = self.error_handler.get(out.status_code, self.default_error_handler)(out)
if isinstance(out, HTTPResponse):
depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9
return self._cast(out)
@@ -1547,9 +1522,35 @@ 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 = self.status
+ response._headers = self._headers
+ 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)
@@ -3166,11 +3167,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 +3179,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>
diff --git a/docs/api.rst b/docs/api.rst
index f2c8129..8322dc3 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 :cls:`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 :cls:`HTTPError`, but not for :cls:`HTTPResponse` or other response types.
+
+.. autoexception:: HTTPResponse
+ :members:
+
+.. autoexception:: HTTPError
:members:
diff --git a/test/test_environ.py b/test/test_environ.py
index aabd929..08d3592 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')
@@ -583,7 +583,7 @@ 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:
@@ -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):