From c5d752135305f422e7ecf3024922e78644dd1127 Mon Sep 17 00:00:00 2001 From: Daniel Waardal Date: Thu, 26 Jul 2012 18:59:48 +0300 Subject: Updated url of logo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.1 From 771444217a1643a4b5ad12dbe3241f792a441d9e Mon Sep 17 00:00:00 2001 From: Graham Ashton Date: Sun, 5 Aug 2012 14:52:06 +0200 Subject: Fixed spelling mistake/typo. Changed "refecence" to "reference". --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 9c1bccb..6b49fa9 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 `_ or visit us in our `IRC channel `_. +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 `_ or visit us in our `IRC channel `_. .. _installation: -- cgit v1.2.1 From e3f240ab6965366770227482e048990017ebcc8f Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Fri, 24 Aug 2012 12:16:11 -0300 Subject: Plugin Development tutorial code error Just a small error reported in maillist. --- docs/plugindev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. -- cgit v1.2.1 From 4047cda0ab5aaab2472888c3fe0863edf4bd9bc6 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 16 Aug 2012 18:35:12 +0200 Subject: Whitespace and typos Fixed copyright date --- LICENSE | 2 +- bottle.py | 11 +++++------ docs/changelog.rst | 6 +++--- 3 files changed, 9 insertions(+), 10 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/bottle.py b/bottle.py index b838573..6fe54a1 100644 --- a/bottle.py +++ b/bottle.py @@ -9,7 +9,7 @@ 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) """ @@ -1259,7 +1259,7 @@ class BaseRequest(object): var = self.environ['bottle.request.ext.%s'%name] return var.__get__(self) if hasattr(var, '__get__') else var except KeyError: - raise AttributeError('Attribute %r not defined.' % name) + raise AttributeError('Attribute %r not defined.' % name) def __setattr__(self, name, value): if name == 'environ': return object.__setattr__(self, name, value) @@ -1943,7 +1943,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. @@ -1972,11 +1972,10 @@ 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:: - + res.add_path('./resources/', __file__) ''' base = os.path.abspath(os.path.dirname(base or self.base)) @@ -3200,7 +3199,7 @@ ERROR_PAGE_TEMPLATE = """ %%end """ % __name__ -#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a #: request callback, this instance always refers to the *current* request #: (even on a multithreaded server). request = LocalRequest() diff --git a/docs/changelog.rst b/docs/changelog.rst index cda94fd..dbe387f 100755 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,7 +15,7 @@ Release 0.11 * The new :class:`ResourceManager` interface helps locating files bundled with the application. * Added a server adapter for `waitress `_. * 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. -- cgit v1.2.1 From 49c65935f554c708797300a57ef26c1b0d621c29 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Fri, 17 Aug 2012 00:23:43 +0200 Subject: Replaced HTTPResponse and HTTPError with subclasses of BaseResponse. --- bottle.py | 72 +++++++++++++++++++++++------------------------ docs/api.rst | 29 +++++++++---------- test/test_environ.py | 34 +++++++++++----------- test/test_outputfilter.py | 18 ++++++------ test/test_sendfile.py | 20 ++++++------- test/test_wsgi.py | 6 ++-- 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() - Error {{e.status}}: {{status_name}} + Error: {{e.status}} -

Error {{e.status}}: {{status_name}}

+

Error: {{e.status}}

Sorry, the requested URL {{repr(request.url)}} caused an error:

-
{{e.output}}
+
{{e.body}}
%%if DEBUG and e.exception:

Exception:

{{repr(e.exception)}}
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("",'/') 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): -- cgit v1.2.1 From 9d2f04a3c0aca3ee4c5aa363e0a6a770e10f71a6 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 23 Aug 2012 18:39:42 +0200 Subject: Fix: Bottle no longer changes the Content-Length response header if it was set by the application. (fix #311) --- bottle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bottle.py b/bottle.py index bdafbcf..70782d4 100644 --- a/bottle.py +++ b/bottle.py @@ -782,7 +782,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))\ @@ -793,15 +794,14 @@ 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_code, self.default_error_handler)(out) - if isinstance(out, HTTPResponse): - depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 return self._cast(out) if isinstance(out, HTTPResponse): out.apply(response) -- cgit v1.2.1 From 14020ae680d8309c331f961f52e7b64e35c66d1b Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 23 Aug 2012 23:46:51 +0200 Subject: Some micro-optimisations. --- bottle.py | 48 ++++++++++++++++++++++-------------------------- test/test_environ.py | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/bottle.py b/bottle.py index 70782d4..740ecf5 100644 --- a/bottle.py +++ b/bottle.py @@ -757,7 +757,8 @@ class Bottle(object): 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: @@ -847,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 @@ -1285,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 @@ -1329,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 @@ -1346,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 @@ -1360,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. ''' @@ -1375,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 @@ -1393,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) @@ -1534,8 +1528,10 @@ class HTTPResponse(Response, BottleException): super(HTTPResponse, self).__init__(body, status, **headers) def apply(self, response): - response.status = self.status + 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): diff --git a/test/test_environ.py b/test/test_environ.py index 08d3592..5198b02 100755 --- a/test/test_environ.py +++ b/test/test_environ.py @@ -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) -- cgit v1.2.1 From 2b1ed4f746c28a55c497ff482c9f50d9480bc40f Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 30 Aug 2012 01:58:15 +0200 Subject: Raise RuntimeError when using gevent without monkey-patches. --- bottle.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bottle.py b/bottle.py index 740ecf5..23cd099 100644 --- a/bottle.py +++ b/bottle.py @@ -2488,10 +2488,11 @@ class GeventServer(ServerAdapter): 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() -- cgit v1.2.1 From 5238c615b3ec198fedebb0fcaad4458e3d68d70f Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 30 Aug 2012 01:59:39 +0200 Subject: Removed unused parameter docs from GeventServer --- bottle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bottle.py b/bottle.py index 23cd099..60cd031 100644 --- a/bottle.py +++ b/bottle.py @@ -2483,7 +2483,6 @@ 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. """ -- cgit v1.2.1 From cddc405a8d04220cd339f8d51fc57bfd4dfabdc4 Mon Sep 17 00:00:00 2001 From: Nina Stawski Date: Tue, 11 Sep 2012 11:45:08 -0700 Subject: Fixed code indent for one of the examples --- docs/tutorial_app.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 '

The new task was inserted into the database, the ID is %s

' % new_id - else: - return template('new_task.tpl') + return '

The new task was inserted into the database, the ID is %s

' % new_id + else: + return template('new_task.tpl') ``new_task.tpl`` looks like this:: -- cgit v1.2.1 From d729bcb2d6eecac8b92d60dd547779f5961d49a1 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 5 Sep 2012 02:12:43 +0200 Subject: Fix XSS vulnerability in hello world example --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 'Hello %s!' % name + return template('Hello {{name}}!', name=name) run(host='localhost', port=8080) -- cgit v1.2.1 From 47784117e07a5a60af3d1500070358ee28e6d20d Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 17 Sep 2012 00:36:09 +0200 Subject: use template in tutorial to match index --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 6b49fa9..9ef531d 100755 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -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/') 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. -- cgit v1.2.1 From c5381b6af707e6c7d85e2df3af8f401d074661f5 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Wed, 19 Sep 2012 08:11:13 -0300 Subject: Avoid __main__.ext namespace Fix #334. --- bottle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottle.py b/bottle.py index 60cd031..28b8448 100644 --- a/bottle.py +++ b/bottle.py @@ -3214,7 +3214,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('%s.ext' % 'bottle' if __name__ == '__main__' else __name__, 'bottle_%s').module if __name__ == '__main__': opt, args, parser = _cmd_options, _cmd_args, _cmd_parser -- cgit v1.2.1 From bbfc49b419bbbeb4d16f5ae3f1ef436b34ef305e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 20 Sep 2012 15:48:56 +1000 Subject: Fix bottle.ext namespacing when importing as bottle (regression in fix for #334) --- bottle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottle.py b/bottle.py index 28b8448..f0c1c2a 100644 --- a/bottle.py +++ b/bottle.py @@ -3214,7 +3214,7 @@ app.push() #: A virtual package that redirects import statements. #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect('%s.ext' % 'bottle' if __name__ == '__main__' else __name__, '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 -- cgit v1.2.1 From 7c45f076c1dab4e582581530085824a801970b06 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Mon, 24 Sep 2012 14:25:42 +0200 Subject: plugin.apply: rename context to route --- bottle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bottle.py b/bottle.py index f0c1c2a..e8df242 100644 --- a/bottle.py +++ b/bottle.py @@ -1565,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): @@ -1615,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') -- cgit v1.2.1 From 96645e845b87d87cf3fc4c01b105ff4240c36108 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 4 Oct 2012 19:21:16 +0200 Subject: Fix #382 "Test failures with Python 3" --- bottle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bottle.py b/bottle.py index f0c1c2a..dfda045 100644 --- a/bottle.py +++ b/bottle.py @@ -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): -- cgit v1.2.1 From be36389a1832f124986dd45b2adff3cd1e4f8ece Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Tue, 9 Oct 2012 17:12:39 +0200 Subject: Mostly typos in docs --- bottle.py | 6 +++--- docs/api.rst | 4 ++-- docs/changelog.rst | 2 +- docs/deployment.rst | 7 ++++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bottle.py b/bottle.py index 07e5baf..737829c 100644 --- a/bottle.py +++ b/bottle.py @@ -519,10 +519,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 @@ -1838,7 +1838,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): diff --git a/docs/api.rst b/docs/api.rst index 8322dc3..aeb4ff0 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -130,7 +130,7 @@ The :class:`Request` class wraps a WSGI environment and provides helpful methods .. 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. +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: @@ -151,7 +151,7 @@ The :class:`Response` class stores the HTTP status code as well as headers and c :members: -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. +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: 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 `_. * 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 `_ 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. -- cgit v1.2.1 From a33e8bf369215218f3cbbff069f6c29860bd2dfa Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Tue, 9 Oct 2012 19:31:03 +0200 Subject: Release of 0.11.1 --- bottle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottle.py b/bottle.py index 737829c..7ee9621 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.rc1' +__version__ = '0.11.1' __license__ = 'MIT' # The gevent server adapter needs to patch some modules before they are imported -- cgit v1.2.1 From 5c86070bc4b176e20288adcdf5ff2633a33f9848 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Tue, 9 Oct 2012 19:41:50 +0200 Subject: Start of 0.12 development --- bottle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottle.py b/bottle.py index 7ee9621..bac545a 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.1' +__version__ = '0.12-dev' __license__ = 'MIT' # The gevent server adapter needs to patch some modules before they are imported -- cgit v1.2.1 From 21dafedf88438f34dcdfb3d4160a7988093051e6 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Wed, 10 Oct 2012 17:20:15 +0200 Subject: fix #383: Bottle raises DeprecationWarning - internal --- bottle.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/bottle.py b/bottle.py index bac545a..9cb0979 100644 --- a/bottle.py +++ b/bottle.py @@ -807,7 +807,7 @@ class Bottle(object): 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'): @@ -2066,7 +2066,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.") @@ -2077,41 +2077,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) -- cgit v1.2.1 From 611d997d93c701c8e10f6f8a162023cedc92b003 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 11 Oct 2012 20:52:23 +0200 Subject: fix #382: "dictionary changed size during iteration" bug in test case. --- test/test_environ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_environ.py b/test/test_environ.py index 5198b02..930280c 100755 --- a/test/test_environ.py +++ b/test/test_environ.py @@ -586,7 +586,7 @@ 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] -- cgit v1.2.1 From e371da5e2e1f6cda2edb2f062bb0824c2330b06c Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 11 Oct 2012 22:55:30 +0200 Subject: Makefile: A release always pushes to public repository now. --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ba4722..9bc4d83 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,12 @@ 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 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 -- cgit v1.2.1 From 9e374eefda3eb2d226ed8dbc4f18eda530583e33 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Wed, 17 Oct 2012 21:05:35 +0200 Subject: Fix #387: Template lookup found files in workdir, even if TEMPLATE_PATH did not contain '.'. The template path list is now obeyed (with an exception for templates with an explicit absolute path). The next release will tighten the rules a bit further: TEMPLATE_PATH must not be empty, and absolute paths are considered relative, too. This ensures that only templates from within TEMPLATE_PATH directries are loaded. --- bottle.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bottle.py b/bottle.py index 9cb0979..6432d4f 100644 --- a/bottle.py +++ b/bottle.py @@ -2799,11 +2799,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) -- cgit v1.2.1 From 94f5962506772f75adb1b84bf6684f57c321be07 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Wed, 17 Oct 2012 22:50:51 +0200 Subject: fix #389: Bottle still raises DeprecationWarning in bottle.redirect() and Router.match() --- bottle.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bottle.py b/bottle.py index 6432d4f..0c4b788 100644 --- a/bottle.py +++ b/bottle.py @@ -407,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): @@ -2045,7 +2044,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): -- cgit v1.2.1 From 26ac343788ac4dd2bbdcf8d6734411a495571b17 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Wed, 17 Oct 2012 23:30:09 +0200 Subject: Fixed release rule in Makefile and added test_33 --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9bc4d83..40b5384 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ 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 tag $(VERSION) # Fail on dublicate tag + git push origin tag $(VERSION) # Fail on dublicate tag python setup.py sdist register upload # Release to pypi push: test_all @@ -24,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 @@ -41,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 {} + -- cgit v1.2.1