diff options
author | Marcel Hellkamp <marc@gsites.de> | 2012-08-17 00:23:43 +0200 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2012-08-30 01:41:42 +0200 |
commit | 49c65935f554c708797300a57ef26c1b0d621c29 (patch) | |
tree | 506e053daf84fc7395e9c5330751957ad37e0bd0 | |
parent | 4047cda0ab5aaab2472888c3fe0863edf4bd9bc6 (diff) | |
download | bottle-49c65935f554c708797300a57ef26c1b0d621c29.tar.gz |
Replaced HTTPResponse and HTTPError with subclasses of BaseResponse.
-rw-r--r-- | bottle.py | 72 | ||||
-rwxr-xr-x | docs/api.rst | 29 | ||||
-rwxr-xr-x | test/test_environ.py | 34 | ||||
-rwxr-xr-x | test/test_outputfilter.py | 18 | ||||
-rwxr-xr-x | test/test_sendfile.py | 20 | ||||
-rwxr-xr-x | test/test_wsgi.py | 6 |
6 files changed, 88 insertions, 91 deletions
@@ -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): |