From 1a7f579ece2528e73d9959d6fe4c8d7172fc3959 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Mon, 14 Mar 2011 23:32:33 -0400 Subject: [PATCH 01/29] Improved botched docstring wording for silent failure. --- flask/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/config.py b/flask/config.py index b276f168..b588dfa1 100644 --- a/flask/config.py +++ b/flask/config.py @@ -89,7 +89,7 @@ class Config(dict): app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. :return: bool. `True` if able to load config, `False` otherwise. """ @@ -113,7 +113,7 @@ class Config(dict): :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the root path. - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. .. versionadded:: 0.7 From 0ecc686372f577ccbcbcb818e1386ceb7bd9391d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 Apr 2011 19:03:06 +0200 Subject: [PATCH 02/29] Fixed a typo --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index ed8a5d5c..458d6252 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -321,7 +321,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if not attachment_filename and not mimetype \ and isinstance(filename, basestring): warn(DeprecationWarning('The filename support for file objects ' - 'passed to send_file is not deprecated. Pass an ' + 'passed to send_file is now deprecated. Pass an ' 'attach_filename if you want mimetypes to be guessed.'), stacklevel=2) if add_etags: From a2225bf57e7c20f5c63c3c2b79db397f3724c1dc Mon Sep 17 00:00:00 2001 From: Noufal Ibrahim Date: Mon, 28 Mar 2011 17:01:21 +0530 Subject: [PATCH 03/29] Added a note on actually starting the application Signed-off-by: Armin Ronacher --- docs/tutorial/setup.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 64bf3b66..e9e4d679 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -70,7 +70,14 @@ server if we want to run that file as a standalone application:: app.run() With that out of the way you should be able to start up the application -without problems. When you head over to the server you will get an 404 +without problems. Do this with the following command:: + + python flaskr.py + +You will see a message telling you that server has started along with +the address at which you can access it. + +When you head over to the server in your browser you will get an 404 page not found error because we don't have any views yet. But we will focus on that a little later. First we should get the database working. From 709ecefee1859cf16cc8bdfa642e39fbc6b4421e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Roy?= Date: Wed, 30 Mar 2011 22:58:41 -0400 Subject: [PATCH 04/29] Updating documentation for app.after_request decorator. Signed-off-by: Armin Ronacher --- flask/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 49c8fe10..20fa42d9 100644 --- a/flask/app.py +++ b/flask/app.py @@ -713,7 +713,10 @@ class Flask(_PackageBoundObject): return f def after_request(self, f): - """Register a function to be run after each request.""" + """Register a function to be run after each request. Your function + must take one parameter, a :attr:`response_class` object and return + a new response object or the same (see :meth:`process_response`). + """ self.after_request_funcs.setdefault(None, []).append(f) return f From 0e4cd2e6512c66ba004df0f35f3a21e8cfcdf77c Mon Sep 17 00:00:00 2001 From: Kanak Kshetri Date: Thu, 31 Mar 2011 15:53:17 -0400 Subject: [PATCH 05/29] Fixed a typo that was preventing second code block from appearing in a code block Signed-off-by: Armin Ronacher --- docs/tutorial/dbcon.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index f700a329..b2a626f9 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -24,7 +24,7 @@ db connection in the interactive debugger:: return response If you want to guarantee that the connection is always closed in debug mode, you -can close it in a function decorated with :meth:`~flask.Flask.teardown_request`: +can close it in a function decorated with :meth:`~flask.Flask.teardown_request`:: @app.before_request def before_request(): From 7ed3196e8d1861acd027c409c47e51cce812ba63 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 4 Apr 2011 16:53:04 +0200 Subject: [PATCH 06/29] Add safe_join: returns the filename used by send_from_directory. Signed-off-by: Armin Ronacher --- docs/api.rst | 2 ++ flask/__init__.py | 2 +- flask/helpers.py | 34 +++++++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c2d90ce6..88d026ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -244,6 +244,8 @@ Useful Functions and Classes .. autofunction:: send_from_directory +.. autofunction:: safe_join + .. autofunction:: escape .. autoclass:: Markup diff --git a/flask/__init__.py b/flask/__init__.py index 3a232e5e..1274e766 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -19,7 +19,7 @@ from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute, make_response + get_template_attribute, make_response, safe_join from .globals import current_app, g, request, session, _request_ctx_stack from .ctx import has_request_context from .module import Module diff --git a/flask/helpers.py b/flask/helpers.py index 458d6252..9d9af4bf 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -388,6 +388,32 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv +def safe_join(directory, filename): + """Safely join `directory` and `filename`. + + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path + would fall out of `directory`. + + Example usage:: + + @app.route('/wiki/') + def wiki_page(filename): + filename = safe_join(app.config['WIKI_FOLDER'], filename) + with open(filename, 'rb') as fd: + content = fd.read() # Read and process the file content... + + """ + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): + raise NotFound() + return os.path.join(directory, filename) + + def send_from_directory(directory, filename, **options): """Send a file from a given directory with :func:`send_file`. This is a secure way to quickly expose static files from an upload folder @@ -415,13 +441,7 @@ def send_from_directory(directory, filename, **options): :param options: optional keyword arguments that are directly forwarded to :func:`send_file`. """ - filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - raise NotFound() - if os.path.isabs(filename) or filename.startswith('../'): - raise NotFound() - filename = os.path.join(directory, filename) + filename = safe_join(directory, filename) if not os.path.isfile(filename): raise NotFound() return send_file(filename, conditional=True, **options) From 3c7b5a68f1ede9c870ae76f44e2dde6102843e54 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 16:48:40 +0200 Subject: [PATCH 07/29] Changelog entry --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 9420dac6..f5c70477 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,7 @@ Release date to be announced, codename to be selected - Added `teardown_request` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` +- Added :func:`safe_join` Version 0.6.1 ------------- From 1dd83964f0c1fb3c4772cf24880f5aeb3f282f66 Mon Sep 17 00:00:00 2001 From: streety Date: Mon, 18 Apr 2011 02:49:31 -0700 Subject: [PATCH 08/29] Change to match headers in https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/contrib/fixers.py Signed-off-by: Armin Ronacher --- docs/deploying/others.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 8f08ecd1..153fb7cf 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -72,7 +72,7 @@ problematic values in the WSGI environment usually are `REMOTE_ADDR` and but you might want to write your own WSGI middleware for specific setups. The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forward-For`:: +and the remote address from `X-Forwarded-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) From d8fcd4260e9194a03267ddb913a8b5d7cf0948d2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 23:19:59 +0200 Subject: [PATCH 09/29] Whitespace normalization --- flask/helpers.py | 2 +- tests/flask_tests.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 9d9af4bf..e04b7f28 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -390,7 +390,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, def safe_join(directory, filename): """Safely join `directory` and `filename`. - + :param directory: the base directory. :param filename: the untrusted filename relative to that directory. :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 19168180..265f89f1 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -642,6 +642,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): app.config.update( SERVER_NAME='localhost.localdomain:5000' ) + @app.route('/') def index(): return None From e774e3a69eeb9a811a22e73ff87267e8bf4f9027 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 23:20:24 +0200 Subject: [PATCH 10/29] Switch params and example --- flask/helpers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index e04b7f28..418c4b17 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -391,11 +391,6 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, def safe_join(directory, filename): """Safely join `directory` and `filename`. - :param directory: the base directory. - :param filename: the untrusted filename relative to that directory. - :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path - would fall out of `directory`. - Example usage:: @app.route('/wiki/') @@ -404,6 +399,10 @@ def safe_join(directory, filename): with open(filename, 'rb') as fd: content = fd.read() # Read and process the file content... + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path + would fall out of `directory`. """ filename = posixpath.normpath(filename) for sep in _os_alt_seps: From 017117778ec477205eaba6acab951951808a60a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 25 Apr 2011 16:02:35 +0200 Subject: [PATCH 11/29] Document that None skips in query strings. This fixes #224 --- flask/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 418c4b17..ec513d8f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -166,7 +166,8 @@ def url_for(endpoint, **values): ==================== ======================= ============================= Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. + to the generated URL as query arguments. If the value of a query argument + is `None`, the whole pair is skipped. For more information, head over to the :ref:`Quickstart `. From c6e4d743a93d8999a8d95044b6d89132e25cb7d9 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Wed, 27 Apr 2011 14:28:07 -0700 Subject: [PATCH 12/29] A couple of corrections to the example fabfile. --- docs/patterns/fabric.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index 49be85ab..dbd4f913 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -32,7 +32,7 @@ hosts. These hosts can be defined either in the fabfile or on the command line. In this case we will add them to the fabfile. This is a basic first example that has the ability to upload the current -sourcecode to the server and install it into a already existing +sourcecode to the server and install it into a pre-existing virtual environment:: from fabric.api import * @@ -53,12 +53,12 @@ virtual environment:: put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it - run('mkdir yourapplication') + run('mkdir /tmp/yourapplication') with cd('/tmp/yourapplication'): run('tar xzf /tmp/yourapplication.tar.gz') - # now setup the package with our virtual environment's - # python interpreter - run('/var/www/yourapplication/env/bin/python setup.py install') + # now setup the package with our virtual environment's + # python interpreter + run('/var/www/yourapplication/env/bin/python setup.py install') # now that all is set up, delete the folder again run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz') # and finally touch the .wsgi file so that mod_wsgi triggers From 15c937b316b16b12df2375c706bcc93e3d37d33e Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 May 2011 16:36:51 -0400 Subject: [PATCH 13/29] Remove ctx.bind() from doc. It doesn't exist. --- flask/app.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/flask/app.py b/flask/app.py index 20fa42d9..1e068e41 100644 --- a/flask/app.py +++ b/flask/app.py @@ -928,15 +928,6 @@ class Flask(_PackageBoundObject): finally: ctx.pop() - The big advantage of this approach is that you can use it without - the try/finally statement in a shell for interactive testing: - - >>> ctx = app.test_request_context() - >>> ctx.bind() - >>> request.path - u'/' - >>> ctx.unbind() - .. versionchanged:: 0.3 Added support for non-with statement usage and `with` statement is now passed the ctx object. From 74514fc83795d5ca705f67333993b5c7f9d01316 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 8 May 2011 11:31:52 +0200 Subject: [PATCH 14/29] the import order note was meant to be bold, not cursive --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index e1b92c08..28cd70e4 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -57,7 +57,7 @@ following quick checklist: 2. all the view functions (the ones with a :meth:`~flask.Flask.route` decorator on top) have to be imported when in the `__init__.py` file. Not the object itself, but the module it is in. Import the view module - *after the application object is created*. + **after the application object is created**. Here's an example `__init__.py`:: From 8b974eb35523fde507055a27f70484133bb21b79 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 12 May 2011 12:54:56 -0400 Subject: [PATCH 15/29] Note to use debug=False for third-party debuggers. As requested on mailing list. http://flask.pocoo.org/mailinglist/archive/2011/5/12/using-eclipse%2Bpydev-for-debugging-flask-apps/ --- docs/quickstart.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b4f6027f..c18c2332 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -113,6 +113,11 @@ Screenshot of the debugger in action: :class: screenshot :alt: screenshot of debugger in action +.. admonition:: Working With Other Debuggers + + Some third-party debuggers, e.g. PyDev and IntelliJ, are interrupted when + ``app`` reloads. To use these debuggers, set ``app.debug = False``. + Routing ------- From 57920a5808c03d7d035f592a13a71ba627fe85da Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 17 May 2011 11:41:12 -0400 Subject: [PATCH 16/29] Prevent pop if flashes not in session to avoid modification to session fixes #227 --- flask/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index ec513d8f..a25dcadd 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -249,7 +249,8 @@ def get_flashed_messages(with_categories=False): """ flashes = _request_ctx_stack.top.flashes if flashes is None: - _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ + if '_flashes' in session else [] if not with_categories: return [x[1] for x in flashes] return flashes From ea77d5e12d5148e8f42e449deac64f363c5db484 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 19 May 2011 09:14:53 -0400 Subject: [PATCH 17/29] Touch up docs according to user feedback. --- docs/quickstart.rst | 4 ++-- flask/config.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c18c2332..fc5ae0f1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -115,8 +115,8 @@ Screenshot of the debugger in action: .. admonition:: Working With Other Debuggers - Some third-party debuggers, e.g. PyDev and IntelliJ, are interrupted when - ``app`` reloads. To use these debuggers, set ``app.debug = False``. + Debuggers interfere with each other. If you are using another debugger + (e.g. PyDev or IntelliJ), you may need to set ``app.debug = False``. Routing diff --git a/flask/config.py b/flask/config.py index b588dfa1..bb2d6e9e 100644 --- a/flask/config.py +++ b/flask/config.py @@ -141,8 +141,8 @@ class Config(dict): Objects are usually either modules or classes. - Just the uppercase variables in that object are stored in the config - after lowercasing. Example usage:: + Just the uppercase variables in that object are stored in the config. + Example usage:: app.config.from_object('yourapplication.default_config') from yourapplication import default_config From 41952d2b259efdd89634aef7f47afc4cef6e7190 Mon Sep 17 00:00:00 2001 From: Steve Romanow Date: Wed, 18 May 2011 18:16:48 -0400 Subject: [PATCH 18/29] respect request charset Signed-off-by: Armin Ronacher --- flask/wrappers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 4db1e782..557ab841 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -73,7 +73,13 @@ class Request(RequestBase): if __debug__: _assert_have_json() if self.mimetype == 'application/json': - return json.loads(self.data) + request_charset = self.mimetype_params.get('charset') + if request_charset is not None: + j = json.loads(self.data, encoding=request_charset ) + else: + j = json.loads(self.data) + + return j class Response(ResponseBase): From d90765b0265eec81130172efe3250cd1e3c900bd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:29:46 +0200 Subject: [PATCH 19/29] Added testcase for json encoding parameter support --- tests/flask_tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 265f89f1..6b6daaa4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -785,6 +785,18 @@ class BasicFunctionalityTestCase(unittest.TestCase): class JSONTestCase(unittest.TestCase): + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + assert resp.data == u'Hällo Wörld'.encode('utf-8') + def test_jsonify(self): d = dict(a=23, b=42, c=[1, 2, 3]) app = flask.Flask(__name__) From 2a81c8a822e166299fed27a403a59a004f689d37 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:30:08 +0200 Subject: [PATCH 20/29] Documented change --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index f5c70477..f7645d71 100644 --- a/CHANGES +++ b/CHANGES @@ -41,6 +41,8 @@ Release date to be announced, codename to be selected of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` - Added :func:`safe_join` +- The automatic JSON request data unpacking now looks at the charset + mimetype parameter. Version 0.6.1 ------------- From 7242abcfb22c5b7321dc658766e8b6f5486ff1c3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:34:41 +0200 Subject: [PATCH 21/29] Extend the logging from the dynamically set logger class. This fixes #234 --- flask/logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/logging.py b/flask/logging.py index 29caadce..8379ab66 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -11,7 +11,7 @@ from __future__ import absolute_import -from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG +from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG def create_logger(app): @@ -21,6 +21,7 @@ def create_logger(app): function also removes all attached handlers in case there was a logger with the log name before. """ + Logger = getLoggerClass() class DebugLogger(Logger): def getEffectiveLevel(x): From 34e6933832f16a2031691b7397e42d9d157bd054 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:42:06 +0200 Subject: [PATCH 22/29] Documented change in flashing --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index f7645d71..e10744ca 100644 --- a/CHANGES +++ b/CHANGES @@ -43,6 +43,8 @@ Release date to be announced, codename to be selected - Added :func:`safe_join` - The automatic JSON request data unpacking now looks at the charset mimetype parameter. +- Don't modify the session on :func:`flask.get_flashed_messages` if there + are no messages in the session. Version 0.6.1 ------------- From e3f2dd8f080115d626437bbee3631f7cf5560ca5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 15:59:11 +0200 Subject: [PATCH 23/29] Added a test for content length behavior --- tests/flask_tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6b6daaa4..b723f217 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -782,6 +782,22 @@ class BasicFunctionalityTestCase(unittest.TestCase): t.start() t.join() + def test_max_content_length(self): + app = flask.Flask(__name__) + app.debug = True + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + assert False + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + assert rv.data == '42' + class JSONTestCase(unittest.TestCase): From e71a5ff8de93801c30ed6daecac4b8502aa86813 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:10:53 +0200 Subject: [PATCH 24/29] Started work on new request dispatching. Unittests not yet updated --- docs/api.rst | 51 ++-------- docs/config.rst | 53 ++++++---- docs/contents.rst.inc | 1 + docs/reqcontext.rst | 230 ++++++++++++++++++++++++++++++++++++++++++ docs/shell.rst | 65 +++++------- docs/signals.rst | 16 +++ flask/__init__.py | 2 +- flask/app.py | 130 ++++++++++++++++-------- flask/ctx.py | 35 ++++++- flask/signals.py | 1 + tests/flask_tests.py | 5 +- 11 files changed, 436 insertions(+), 153 deletions(-) create mode 100644 docs/reqcontext.rst diff --git a/docs/api.rst b/docs/api.rst index 88d026ed..b3953537 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -310,6 +310,9 @@ Configuration Useful Internals ---------------- +.. autoclass:: flask.ctx.RequestContext + :members: + .. data:: _request_ctx_stack The internal :class:`~werkzeug.local.LocalStack` that is used to implement @@ -347,23 +350,6 @@ Useful Internals if ctx is not None: return ctx.session - .. versionchanged:: 0.4 - - The request context is automatically popped at the end of the request - for you. In debug mode the request context is kept around if - exceptions happen so that interactive debuggers have a chance to - introspect the data. With 0.4 this can also be forced for requests - that did not fail and outside of `DEBUG` mode. By setting - ``'flask._preserve_context'`` to `True` on the WSGI environment the - context will not pop itself at the end of the request. This is used by - the :meth:`~flask.Flask.test_client` for example to implement the - deferred cleanup functionality. - - You might find this helpful for unittests where you need the - information from the context local around for a little longer. Make - sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in - that situation, otherwise your unittests will leak memory. - Signals ------- @@ -401,6 +387,12 @@ Signals in debug mode, where no exception handling happens. The exception itself is passed to the subscriber as `exception`. +.. data:: request_tearing_down + + This signal is sent when the application is tearing down the request. + This is always called, even if an error happened. No arguments are + provided. + .. currentmodule:: None .. class:: flask.signals.Namespace @@ -418,28 +410,3 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker - -.. _notes-on-proxies: - -Notes On Proxies ----------------- - -Some of the objects provided by Flask are proxies to other objects. The -reason behind this is that these proxies are shared between threads and -they have to dispatch to the actual object bound to a thread behind the -scenes as necessary. - -Most of the time you don't have to care about that, but there are some -exceptions where it is good to know that this object is an actual proxy: - -- The proxy objects do not fake their inherited types, so if you want to - perform actual instance checks, you have to do that on the instance - that is being proxied (see `_get_current_object` below). -- if the object reference is important (so for example for sending - :ref:`signals`) - -If you need to get access to the underlying object that is proxied, you -can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: - - app = current_app._get_current_object() - my_signal.send(app) diff --git a/docs/config.rst b/docs/config.rst index 90a276cc..aedaae7d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -51,27 +51,36 @@ The following configuration values are used internally by Flask: .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| -=============================== ========================================= -``DEBUG`` enable/disable debug mode -``TESTING`` enable/disable testing mode -``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the - propagation of exceptions. If not set or - explicitly set to `None` this is - implicitly true if either `TESTING` or - `DEBUG` is true. -``SECRET_KEY`` the secret key -``SESSION_COOKIE_NAME`` the name of the session cookie -``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as - :class:`datetime.timedelta` object. -``USE_X_SENDFILE`` enable/disable x-sendfile -``LOGGER_NAME`` the name of the logger -``SERVER_NAME`` the name of the server. Required for - subdomain support (e.g.: ``'localhost'``) -``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will - reject incoming requests with a - content length greater than this by - returning a 413 status code. -=============================== ========================================= +================================= ========================================= +``DEBUG`` enable/disable debug mode +``TESTING`` enable/disable testing mode +``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the + propagation of exceptions. If not set or + explicitly set to `None` this is + implicitly true if either `TESTING` or + `DEBUG` is true. +``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in + debug mode the request context is not + popped on exceptions to enable debuggers + to introspect the data. This can be + disabled by this key. You can also use + this setting to force-enable it for non + debug execution which might be useful to + debug production applications (but also + very risky). +``SECRET_KEY`` the secret key +``SESSION_COOKIE_NAME`` the name of the session cookie +``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as + :class:`datetime.timedelta` object. +``USE_X_SENDFILE`` enable/disable x-sendfile +``LOGGER_NAME`` the name of the logger +``SERVER_NAME`` the name of the server. Required for + subdomain support (e.g.: ``'localhost'``) +``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will + reject incoming requests with a + content length greater than this by + returning a 413 status code. +================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -102,7 +111,7 @@ The following configuration values are used internally by Flask: ``MAX_CONTENT_LENGTH`` .. versionadded:: 0.7 - ``PROPAGATE_EXCEPTIONS`` + ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` Configuring from Files ---------------------- diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index f32d1da5..2689b4b5 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,6 +17,7 @@ instructions for web development with Flask. errorhandling config signals + reqcontext shell patterns/index deploying/index diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst new file mode 100644 index 00000000..088502eb --- /dev/null +++ b/docs/reqcontext.rst @@ -0,0 +1,230 @@ +.. _request-context: + +The Request Context +=================== + +This document describes the behavior in Flask 0.7 which is mostly in line +with the old behavior but has some small, subtle differences. + +One of the design ideas behind Flask is that there are two different +“states” in which code is executed. The application setup state in which +the application implicitly is on the module level. It starts when the +:class:`Flask` object is instantiated, and it implicitly ends when the +first request comes in. While the application is in this state a few +assumptions are true: + +- the programmer can modify the application object safely. +- no request handling happened so far +- you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +On the contrast, during request handling, a couple of other rules exist: + +- while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. +- any code can get hold of these objects at any time. + +The magic that makes this works is internally referred in Flask as the +“request context”. + +Diving into Context Locals +-------------------------- + +Say you have a utility function that returns the URL the user should be +redirected to. Imagine it would always redirect to the URL's ``next`` +parameter or the HTTP referrer or the index page:: + + from flask import request, url_for + + def redirect_url(): + return request.args.get('next') or \ + request.referrer or \ + url_for('index') + +As you can see, it accesses the request object. If you try to run this +from a plain Python shell, this is the exception you will see: + +>>> redirect_url() +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'NoneType' object has no attribute 'request' + +That makes a lot of sense because we currently do not have a request we +could access. So we have to make a request and bind it to the current +context. The :attr:`~flask.Flask.test_request_context` method can create +us a :class:`~flask.ctx.RequestContext`: + +>>> ctx = app.test_request_context('/?next=http://example.com/') + +This context can be used in two ways. Either with the `with` statement +or by calling the :meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods: + +>>> ctx.push() + +From that point onwards you can work with the request object: + +>>> redirect_url() +u'http://example.com/' + +Until you call `pop`: + +>>> ctx.pop() + +Because the request context is internally maintained as a stack you can +push and pop multiple times. This is very handy to implement things like +internal redirects. + +For more information of how to utilize the request context from the +interactive Python shell, head over to the :ref:`shell` chapter. + +How the Context Works +--------------------- + +If you look into how the Flask WSGI application internally works, you will +find a piece of code that looks very much like this:: + + def wsgi_app(self, environ): + with self.request_context(environ): + try: + response = self.full_dispatch_request() + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + +The method :meth:`~Flask.request_context` returns a new +:class:`~flask.ctx.RequestContext` object and uses it in combination with +the `with` statement to bind the context. Everything that is called from +the same thread from this point onwards until the end of the `with` +statement will have access to the request globals (:data:`flask.request` +and others). + +The request context internally works like a stack: The topmost level on +the stack is the current active request. +:meth:`~flask.ctx.RequestContext.push` adds the context to the stack on +the very top, :meth:`~flask.ctx.RequestContext.pop` removes it from the +stack again. On popping the application's +:func:`~flask.Flask.teardown_request` functions are also executed. + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +What happens if an error occurs in Flask during request processing? This +particular behavior changed in 0.7 because we wanted to make it easier to +understand what is actually happening. The new behavior is quite simple: + +1. Before each request, :meth:`~flask.Flask.before_request` functions are + executed. If one of these functions return a response, the other + functions are no longer called. In any case however the return value + is treated as a replacement for the view's return value. + +2. If the :meth:`~flask.Flask.before_request` functions did not return a + response, the regular request handling kicks in and the view function + that was matched has the chance to return a response. + +3. The return value of the view is then converted into an actual response + object and handed over to the :meth:`~flask.Flask.after_request` + functions which have the chance to replace it or modify it in place. + +4. At the end of the request the :meth:`~flask.Flask.teardown_request` + functions are executed. This always happens, even in case of an + unhandled exception down the road. + +Now what happens on errors? In production mode if an exception is not +caught, the 500 internal server handler is called. In development mode +however the exception is not further processed and bubbles up to the WSGI +server. That way things like the interactive debugger can provide helpful +debug information. + +An important change in 0.7 is that the internal server error is now no +longer post processed by the after request callbacks and after request +callbacks are no longer guaranteed to be executed. This way the internal +dispatching code looks cleaner and is easier to customize and understand. + +The new teardown functions are supposed to be used as a replacement for +things that absolutely need to happen at the end of request. + +Teardown Callbacks +------------------ + +The teardown callbacks are special callbacks in that they are executed at +at different point. Strictly speaking they are independent of the actual +request handling as they are bound to the lifecycle of the +:class:`~flask.ctx.RequestContext` object. When the request context is +popped, the :meth:`~flask.Flask.teardown_request` functions are called. + +This is important to know if the life of the request context is prolonged +by using the test client in a with statement of when using the request +context from the command line:: + + with app.test_client() as client: + resp = client.get('/foo') + # the teardown functions are still not called at that point + # even though the response ended and you have the response + # object in your hand + + # only when the code reaches this point the teardown functions + # are called. Alternatively the same thing happens if another + # request was triggered from the test client + +It's easy to see the behavior from the command line: + +>>> app = Flask(__name__) +>>> @app.teardown_request +... def after_request(exception=None): +... print 'after request' +... +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> ctx.pop() +after request + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +reason behind this is that these proxies are shared between threads and +they have to dispatch to the actual object bound to a thread behind the +scenes as necessary. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is an actual proxy: + +- The proxy objects do not fake their inherited types, so if you want to + perform actual instance checks, you have to do that on the instance + that is being proxied (see `_get_current_object` below). +- if the object reference is important (so for example for sending + :ref:`signals`) + +If you need to get access to the underlying object that is proxied, you +can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) + +Context Preservation on Error +----------------------------- + +If an error occurs or not, at the end of the request the request context +is popped and all data associated with it is destroyed. During +development however that can be problematic as you might want to have the +information around for a longer time in case an exception occurred. In +Flask 0.6 and earlier in debug mode, if an exception occurred, the +request context was not popped so that the interactive debugger can still +provide you with important information. + +Starting with Flask 0.7 you have finer control over that behavior by +setting the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. By +default it's linked to the setting of ``DEBUG``. If the application is in +debug mode the context is preserved, in production mode it's not. + +Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` in production mode +as it will cause your application to leak memory on exceptions. However +it can be useful during development to get the same error preserving +behavior as in development mode when attempting to debug an error that +only occurs under production settings. diff --git a/docs/shell.rst b/docs/shell.rst index 470bceca..61b9dc05 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,3 +1,5 @@ +.. _shell: + Working with the Shell ====================== @@ -21,61 +23,37 @@ that these functions are not only there for interactive shell usage, but also for unittesting and other situations that require a faked request context. -Diving into Context Locals --------------------------- - -Say you have a utility function that returns the URL the user should be -redirected to. Imagine it would always redirect to the URL's ``next`` -parameter or the HTTP referrer or the index page:: - - from flask import request, url_for - - def redirect_url(): - return request.args.get('next') or \ - request.referrer or \ - url_for('index') - -As you can see, it accesses the request object. If you try to run this -from a plain Python shell, this is the exception you will see: +Generally it's recommended that you read the :ref:`request-context` +chapter of the documentation first. ->>> redirect_url() -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'NoneType' object has no attribute 'request' +Creating a Request Context +-------------------------- -That makes a lot of sense because we currently do not have a request we -could access. So we have to make a request and bind it to the current -context. The :attr:`~flask.Flask.test_request_context` method can create -us a request context: +The easiest way to create a proper request context from the shell is by +using the :attr:`~flask.Flask.test_request_context` method which creates +us a :class:`~flask.ctx.RequestContext`: ->>> ctx = app.test_request_context('/?next=http://example.com/') +>>> ctx = app.test_request_context() -This context can be used in two ways. Either with the `with` statement -(which unfortunately is not very handy for shell sessions). The -alternative way is to call the `push` and `pop` methods: +Normally you would use the `with` statement to make this request object +active, but in the shell it's easier to use the +:meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods by hand: >>> ctx.push() -From that point onwards you can work with the request object: - ->>> redirect_url() -u'http://example.com/' - -Until you call `pop`: +From that point onwards you can work with the request object until you +call `pop`: >>> ctx.pop() ->>> redirect_url() -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'NoneType' object has no attribute 'request' - Firing Before/After Request --------------------------- By just creating a request context, you still don't have run the code that -is normally run before a request. This probably results in your database -being unavailable, the current user not being stored on the +is normally run before a request. This might result in your database +being unavailable if you are connecting to the database in a +before-request callback or the current user not being stored on the :data:`~flask.g` object etc. This however can easily be done yourself. Just call @@ -96,6 +74,11 @@ a response object: >>> ctx.pop() +The functions registered as :meth:`~flask.Flask.teardown_request` are +automatically called when the context is popped. So this is the perfect +place to automatically tear down resources that were needed by the request +context (such as database connections). + Further Improving the Shell Experience -------------------------------------- diff --git a/docs/signals.rst b/docs/signals.rst index ed5ecd51..a5821603 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -236,4 +236,20 @@ The following signals exist in Flask: from flask import got_request_exception got_request_exception.connect(log_exception, app) +.. data:: flask.request_tearing_down + :noindex: + + This signal is sent when the request is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender): + session.close() + + from flask import request_tearing_down + request_tearing_down.connect(close_db_connection, app) + .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/__init__.py b/flask/__init__.py index 1274e766..f9fcc09f 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -28,7 +28,7 @@ from .session import Session # the signals from .signals import signals_available, template_rendered, request_started, \ - request_finished, got_request_exception + request_finished, got_request_exception, request_tearing_down # only import json if it's available if json_available: diff --git a/flask/app.py b/flask/app.py index 1e068e41..99d7a266 100644 --- a/flask/app.py +++ b/flask/app.py @@ -27,13 +27,14 @@ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import _RequestContext +from .ctx import RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor -from .signals import request_started, request_finished, got_request_exception +from .signals import request_started, request_finished, got_request_exception, \ + request_tearing_down # a lock used for logger initialization _logger_lock = Lock() @@ -126,6 +127,9 @@ class Flask(_PackageBoundObject): #: For example this might activate unittest helpers that have an #: additional runtime cost which should not be enabled by default. #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: #: This attribute can also be configured from the config with the #: `TESTING` configuration key. Defaults to `False`. testing = ConfigAttribute('TESTING') @@ -191,6 +195,7 @@ class Flask(_PackageBoundObject): 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, + 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), @@ -334,6 +339,19 @@ class Flask(_PackageBoundObject): return rv return self.testing or self.debug + @property + def preserve_context_on_exception(self): + """Returns the value of the `PRESERVE_CONTEXT_ON_EXCEPTION` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PRESERVE_CONTEXT_ON_EXCEPTION'] + if rv is not None: + return rv + return self.debug + @property def logger(self): """A :class:`logging.Logger` object for this application. The @@ -713,16 +731,38 @@ class Flask(_PackageBoundObject): return f def after_request(self, f): - """Register a function to be run after each request. Your function + """Register a function to be run after each request. Your function must take one parameter, a :attr:`response_class` object and return a new response object or the same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception ocurred. """ self.after_request_funcs.setdefault(None, []).append(f) return f def teardown_request(self, f): """Register a function to be run at the end of each request, - regardless of whether there was an exception or not. + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necesary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log ocurring errors. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f @@ -770,21 +810,39 @@ class Flask(_PackageBoundObject): return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. """ req = _request_ctx_stack.top.request + if req.routing_exception is not None: + raise req.routing_exception + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) + + def full_dispatch_request(self): + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + """ try: - if req.routing_exception is not None: - raise req.routing_exception - rule = req.url_rule - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if getattr(rule, 'provide_automatic_options', False) \ - and req.method == 'OPTIONS': - return self.make_default_options_response() - # otherwise dispatch to the handler for that endpoint - return self.view_functions[rule.endpoint](**req.view_args) + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() except HTTPException, e: - return self.handle_http_exception(e) + rv = self.handle_http_exception(e) + response = self.make_response(rv) + response = self.process_response(response) + request_finished.send(self, response=response) + return response def make_default_options_response(self): """This method is called to create the default `OPTIONS` response. @@ -894,7 +952,10 @@ class Flask(_PackageBoundObject): def do_teardown_request(self): """Called after the actual request dispatching and will - call every as :meth:`teardown_request` decorated function. + call every as :meth:`teardown_request` decorated function. This is + not actually called by the :class:`Flask` object itself but is always + triggered when the request context is popped. That way we have a + tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) mod = request.module @@ -905,12 +966,13 @@ class Flask(_PackageBoundObject): rv = func(exc) if rv is not None: return rv + request_tearing_down.send(self) def request_context(self, environ): - """Creates a request context from the given environment and binds - it to the current context. This must be used in combination with - the `with` statement because the request is only bound to the - current context for the duration of the `with` block. + """Creates a :class:`~flask.ctx.RequestContext` from the given + environment and binds it to the current context. This must be used in + combination with the `with` statement because the request is only bound + to the current context for the duration of the `with` block. Example usage:: @@ -934,7 +996,7 @@ class Flask(_PackageBoundObject): :param environ: a WSGI environment """ - return _RequestContext(self, environ) + return RequestContext(self, environ) def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see @@ -969,16 +1031,11 @@ class Flask(_PackageBoundObject): Then you still have the original application object around and can continue to call methods on it. - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - .. versionchanged:: 0.7 - The :meth:`teardown_request` functions get called at the very end of - processing the request. If an exception was thrown, it gets passed to - each teardown_request function. + The behavior of the before and after request callbacks was changed + under error conditions and a new callback was added that will + always execute at the end of the request, independent on if an + error ocurred or not. See :ref:`callbacks-and-errors`. :param environ: a WSGI environment :param start_response: a callable accepting a status code, @@ -987,20 +1044,9 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) + response = self.full_dispatch_request() except Exception, e: response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - finally: - self.do_teardown_request() - request_finished.send(self, response=response) return response(environ, start_response) def __call__(self, environ, start_response): diff --git a/flask/ctx.py b/flask/ctx.py index b63f09f1..bc4877cd 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -51,11 +51,34 @@ def has_request_context(): return _request_ctx_stack.top is not None -class _RequestContext(object): +class RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the `_request_ctx_stack` and removed at the end of it. It will create the URL adapter and request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of `DEBUG` mode. By setting + ``'flask._preserve_context'`` to `True` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. """ def __init__(self, app, environ): @@ -74,7 +97,7 @@ class _RequestContext(object): self.request.routing_exception = e def push(self): - """Binds the request context.""" + """Binds the request context to the current context.""" _request_ctx_stack.push(self) # Open the session at the moment that the request context is @@ -85,7 +108,11 @@ class _RequestContext(object): self.session = _NullSession() def pop(self): - """Pops the request context.""" + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + """ + self.app.do_teardown_request() _request_ctx_stack.pop() def __enter__(self): @@ -99,5 +126,5 @@ class _RequestContext(object): # the context can be force kept alive for the test client. # See flask.testing for how this works. if not self.request.environ.get('flask._preserve_context') and \ - (tb is None or not self.app.debug): + (tb is None or not self.app.preserve_context_on_exception): self.pop() diff --git a/flask/signals.py b/flask/signals.py index 22447c7c..4eedf68f 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -47,4 +47,5 @@ _signals = Namespace() template_rendered = _signals.signal('template-rendered') request_started = _signals.signal('request-started') request_finished = _signals.signal('request-finished') +request_tearing_down = _signals.signal('request-tearing-down') got_request_exception = _signals.signal('got-request-exception') diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b723f217..ba3dbcdf 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -784,8 +784,11 @@ class BasicFunctionalityTestCase(unittest.TestCase): def test_max_content_length(self): app = flask.Flask(__name__) - app.debug = True app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + assert False @app.route('/accept', methods=['POST']) def accept_file(): flask.request.form['myfile'] From ba6bf23e0d08ca0b9163b4e5c37b3d2fd274b1b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:12:20 +0200 Subject: [PATCH 25/29] Updated tests --- tests/flask_tests.py | 46 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ba3dbcdf..387a0589 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -398,37 +398,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'after' in evts assert rv == 'request|after' - def test_after_request_errors(self): - app = flask.Flask(__name__) - called = [] - @app.after_request - def after_request(response): - called.append(True) - return response - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 1 - - def test_after_request_handler_error(self): - called = [] - app = flask.Flask(__name__) - @app.after_request - def after_request(response): - called.append(True) - 1/0 - return response - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 1 - def test_teardown_request_handler(self): called = [] app = flask.Flask(__name__) @@ -460,7 +429,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Response' in rv.data assert len(called) == 1 - def test_teardown_request_handler_error(self): called = [] app = flask.Flask(__name__) @@ -494,7 +462,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Internal Server Error' in rv.data assert len(called) == 2 - def test_before_after_request_order(self): called = [] app = flask.Flask(__name__) @@ -547,6 +514,19 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + assert buffer == [] + ctx.pop() + assert buffer == [None] + def test_response_creation(self): app = flask.Flask(__name__) @app.route('/unicode') From a9fc040c39109f86f3ea98440215b4a54f291f53 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:21:41 +0200 Subject: [PATCH 26/29] Updated documentation to use teardown request where appropriate --- docs/extensiondev.rst | 12 +++++------- docs/patterns/sqlalchemy.rst | 10 ++++------ docs/patterns/sqlite3.rst | 32 ++++++++++++++++++++++++++++---- docs/tutorial/dbcon.rst | 31 ++++++++----------------------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index a5cc2aa4..2c79c6ab 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -182,7 +182,7 @@ Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: def __init__(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.after_request(self.after_request) + self.app.teardown_request(self.teardown_request) self.app.before_request(self.before_request) def connect(self): @@ -192,10 +192,9 @@ Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: ctx = _request_ctx_stack.top ctx.sqlite3_db = self.connect() - def after_request(self, response): + def teardown_request(self, exception): ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - return response def get_db(self): ctx = _request_ctx_stack.top @@ -211,7 +210,7 @@ So here's what these lines of code do: 2. We create a class for our extension that requires a supplied `app` object, sets a configuration for the database if it's not there (:meth:`dict.setdefault`), and attaches `before_request` and - `after_request` handlers. + `teardown_request` handlers. 3. Next, we define a `connect` function that opens a database connection. 4. Then we set up the request handlers we bound to the app above. Note here that we're attaching our database connection to the top request context via @@ -264,7 +263,7 @@ Our extension could add an `init_app` function as follows:: def init_app(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.after_request(self.after_request) + self.app.teardown_request(self.teardown_request) self.app.before_request(self.before_request) def connect(self): @@ -274,10 +273,9 @@ Our extension could add an `init_app` function as follows:: ctx = _request_ctx_stack.top ctx.sqlite3_db = self.connect() - def after_request(self, response): + def teardown_request(self, exception): ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - return response def get_db(self): ctx = _request_ctx_stack.top diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 24e9f013..5a33d1f6 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -65,10 +65,9 @@ automatically remove database sessions at the end of the request for you:: from yourapplication.database import db_session - @app.after_request - def shutdown_session(response): + @app.teardown_request + def shutdown_session(exception=None): db_session.remove() - return response Here is an example model (put this into `models.py`, e.g.):: @@ -140,10 +139,9 @@ each request. Put this into your application module:: from yourapplication.database import db_session - @app.after_request - def shutdown_session(response): + @app.teardown_request + def shutdown_session(exception=None): db_session.remove() - return response Here is an example table and model (put this into `models.py`):: diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 68833234..d0ec5a27 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -5,7 +5,7 @@ Using SQLite 3 with Flask In Flask you can implement the opening of database connections at the beginning of the request and closing at the end with the -:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.teardown_request` decorators in combination with the special :class:`~flask.g` object. So here is a simple example of how you can use SQLite 3 with Flask:: @@ -22,10 +22,34 @@ So here is a simple example of how you can use SQLite 3 with Flask:: def before_request(): g.db = connect_db() - @app.after_request - def after_request(response): + @app.teardown_request + def teardown_request(exception): g.db.close() - return response + +Connect on Demand +----------------- + +The downside of this approach is that this will only work if Flask +executed the before-request handlers for you. If you are attempting to +use the database from a script or the interactive Python shell you would +have to do something like this:: + + with app.test_request_context() + app.preprocess_request() + # now you can use the g.db object + +In order to trigger the execution of the connection code. You won't be +able to drop the dependency on the request context this way, but you could +make it so that the application connects when necessary:: + + def get_connection(): + db = getattr(g, '_db', None) + if db is None: + db = g._db = connect_db() + return db + +Downside here is that you have to use ``db = get_connection()`` instead of +just being able to use ``g.db`` directly. .. _easy-querying: diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index b2a626f9..1d9d41f9 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -9,22 +9,8 @@ connection in all our functions so it makes sense to initialize them before each request and shut them down afterwards. Flask allows us to do that with the :meth:`~flask.Flask.before_request`, -:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request` -decorators. In debug mode, if an error is raised, -:meth:`~flask.Flask.after_request` won't be run, and you'll have access to the -db connection in the interactive debugger:: - - @app.before_request - def before_request(): - g.db = connect_db() - - @app.after_request - def after_request(response): - g.db.close() - return response - -If you want to guarantee that the connection is always closed in debug mode, you -can close it in a function decorated with :meth:`~flask.Flask.teardown_request`:: +:meth:`~flask.Flask.teardown_request` and :meth:`~flask.Flask.teardown_request` +decorators:: @app.before_request def before_request(): @@ -36,18 +22,17 @@ can close it in a function decorated with :meth:`~flask.Flask.teardown_request`: Functions marked with :meth:`~flask.Flask.before_request` are called before a request and passed no arguments. Functions marked with -:meth:`~flask.Flask.after_request` are called after a request and +:meth:`~flask.Flask.teardown_request` are called after a request and passed the response that will be sent to the client. They have to return -that response object or a different one. In this case we just return it -unchanged. - -Functions marked with :meth:`~flask.Flask.teardown_request` get called after the +that response object or a different one. They are however not guaranteed +to be executed if an exception is raised, this is where functions marked with +:meth:`~flask.Flask.teardown_request` come in. They get called after the response has been constructed. They are not allowed to modify the request, and their return values are ignored. If an exception occurred while the request was -being processed, it is passed to each function; otherwise, None is passed in. +being processed, it is passed to each function; otherwise, `None` is passed in. We store our current database connection on the special :data:`~flask.g` -object that flask provides for us. This object stores information for one +object that Flask provides for us. This object stores information for one request only and is available from within each function. Never store such things on other objects because this would not work with threaded environments. That special :data:`~flask.g` object does some magic behind From 115d31ddbfac0773794f115eada88a75d651a0f2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:29:03 +0200 Subject: [PATCH 27/29] More documentation updates --- CHANGES | 5 ++++- docs/upgrading.rst | 47 ++++++++++++++++++++++++++++++++++++++++++++++ flask/app.py | 2 ++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e10744ca..d8d12733 100644 --- a/CHANGES +++ b/CHANGES @@ -38,13 +38,16 @@ Release date to be announced, codename to be selected - Added `create_jinja_loader` to override the loader creation process. - Implemented a silent flag for `config.from_pyfile`. - Added `teardown_request` decorator, for functions that should run at the end - of a request regardless of whether an exception occurred. + of a request regardless of whether an exception occurred. Also the behavior + for `after_request` was changed. It's now no longer executed when an exception + is raised. See :ref:`upgrading-to-new-teardown-handling` - Implemented :func:`flask.has_request_context` - Added :func:`safe_join` - The automatic JSON request data unpacking now looks at the charset mimetype parameter. - Don't modify the session on :func:`flask.get_flashed_messages` if there are no messages in the session. +- `before_request` handlers are now able to abort requests with errors. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 17523290..7d6e0f0a 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -22,6 +22,11 @@ installation, make sure to pass it the ``-U`` parameter:: Version 0.7 ----------- +The following backwards incompatible changes exist from 0.6 to 0.7 + +Bug in Request Locals +````````````````````` + Due to a bug in earlier implementations the request local proxies now raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they are unbound. If you caught these exceptions with :exc:`AttributeError` @@ -44,6 +49,48 @@ New code:: return send_file(my_file_object, add_etags=False) +.. _upgrading-to-new-teardown-handling: + +Upgrading to new Teardown Handling +`````````````````````````````````` + +We streamlined the behavior of the callbacks for request handling. For +things that modify the response the :meth:`~flask.Flask.after_request` +decorators continue to work as expected, but for things that absolutely +must happen at the end of request we introduced the new +:meth:`~flask.Flask.teardown_request` decorator. Unfortunately that +change also made after-request work differently under error conditions. +It's not consistently skipped if exceptions happen whereas previously it +might have been called twice to ensure it is executed at the end of the +request. + +If you have database connection code that looks like this:: + + @app.after_request + def after_request(response): + g.db.close() + return response + +You are now encouraged to use this instead:: + + @app.teardown_request + def after_request(exception): + g.db.close() + +On the upside this change greatly improves the internal code flow and +makes it easier to customize the dispatching and error handling. This +makes it now a lot easier to write unit tests as you can prevent closing +down of database connections for a while. You can take advantage of the +fact that the teardown callbacks are called when the response context is +removed from the stack so a test can query the database after request +handling:: + + with app.test_client() as client: + resp = client.get('/') + # g.db is still bound if there is such a thing + + # and here it's gone + Version 0.6 ----------- diff --git a/flask/app.py b/flask/app.py index 99d7a266..c1bcc340 100644 --- a/flask/app.py +++ b/flask/app.py @@ -831,6 +831,8 @@ class Flask(_PackageBoundObject): """Dispatches the request and on top of that performs request pre and postprocessing as well as HTTP exception catching and error handling. + + .. versionadded:: 0.7 """ try: request_started.send(self) From b51ecd7f212588b3d7363db90a4ab8318ec2fa80 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:29:47 +0200 Subject: [PATCH 28/29] Updated examples --- examples/flaskr/flaskr.py | 5 ++--- examples/minitwit/minitwit.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 69953555..361c1aee 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -47,11 +47,10 @@ def before_request(): g.db = connect_db() -@app.after_request -def after_request(response): +@app.teardown_request +def teardown_request(exception): """Closes the database again at the end of the request.""" g.db.close() - return response @app.route('/') diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 7726e9f4..f7c700d3 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -82,11 +82,10 @@ def before_request(): [session['user_id']], one=True) -@app.after_request -def after_request(response): +@app.teardown_request +def teardown_request(exception): """Closes the database again at the end of the request.""" g.db.close() - return response @app.route('/') From 086ecdb91829d43a9b948c6d97524d8de18e2fa4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 28 May 2011 15:11:48 +0200 Subject: [PATCH 29/29] Better reraising of exceptions --- docs/config.rst | 3 ++- docs/extensiondev.rst | 23 +++++++++++++++++++++++ flask/app.py | 13 ++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index aedaae7d..dc761657 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -240,6 +240,7 @@ your configuration files. However here a list of good recommendations: and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and configurations separately to the production server(s). For some - details about how to do that, head over to the :ref:`deploy` pattern. + details about how to do that, head over to the + :ref:`fabric-deployment` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 2c79c6ab..6b407b34 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -290,6 +290,29 @@ and bind their app to the extension in another file:: manager.init_app(app) +End-Of-Request Behavior +----------------------- + +Due to the change in Flask 0.7 regarding functions that are run at the end +of the request your extension will have to be extra careful there if it +wants to continue to support older versions of Flask. The following +pattern is a good way to support both:: + + def close_connection(response): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() + return response + + if hasattr(app, 'teardown_request'): + app.teardown_request(close_connection) + else: + app.after_request(close_connection) + +Strictly speaking the above code is wrong, because teardown functions are +passed the exception and typically don't return anything. However because +the return value is discarded this will just work assuming that the code +in between does not touch the passed parameter. + Learn from Others ----------------- diff --git a/flask/app.py b/flask/app.py index c1bcc340..36f91128 100644 --- a/flask/app.py +++ b/flask/app.py @@ -793,10 +793,21 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ + exc_type, exc_value, tb = sys.exc_info() + got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) + if self.propagate_exceptions: - raise + # if we want to repropagate the exception, we can attempt to + # raise it with the whole traceback in case we can do that + # (the function was actually called from the except part) + # otherwise, we just raise the error again + if exc_value is e: + raise exc_type, exc_value, tb + else: + raise e + self.logger.exception('Exception on %s [%s]' % ( request.path, request.method