From bc00fd1e83f23f57dd6a765b6a4bab2394584ae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 3 Jun 2010 15:26:07 +0200 Subject: [PATCH] Added support for deferred context cleanup. test_client users can now access the context locals after the actual request if the client is used with a with-block. This fixes #59. --- CHANGES | 3 +++ docs/api.rst | 17 +++++++++++++++++ docs/testing.rst | 24 ++++++++++++++++++++++++ flask.py | 39 ++++++++++++++++++++++++++++++++++++--- tests/flask_tests.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 901f08b2..ca066dad 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,9 @@ Release date to be announced, codename to be selected. - :meth:`~flask.Flask.after_request` handlers are now also invoked if the request dies with an exception and an error handling page kicks in. +- test client has not the ability to preserve the request context + for a little longer. This can also be used to trigger custom + requests that do not pop the request stack for testing. Version 0.3.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index 0fb50551..fc6f68fb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -300,3 +300,20 @@ Useful Internals all the context local objects used in Flask. This is a documented instance and can be used by extensions and application code but the use is discouraged in general. + + .. 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. diff --git a/docs/testing.rst b/docs/testing.rst index db2b4188..de14413d 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -218,3 +218,27 @@ All the other objects that are context bound can be used the same. If you want to test your application with different configurations and there does not seem to be a good way to do that, consider switching to application factories (see :ref:`app-factories`). + + +Keeping the Context Around +-------------------------- + +.. versionadded:: 0.4 + +Sometimes it can be helpful to trigger a regular request but keep the +context around for a little longer so that additional introspection can +happen. With Flask 0.4 this is possible by using the +:meth:`~flask.Flask.test_client` with a `with` block:: + + app = flask.Flask(__name__) + + with app.test_client() as c: + rv = c.get('/?foo=42') + assert request.args['foo'] == '42' + +If you would just be using the :meth:`~flask.Flask.test_client` without +the `with` block, the `assert` would fail with an error because `request` +is no longer available (because used outside of an actual request). +Keep in mind however that :meth:`~flask.Flask.after_request` functions +are already called at that point so your database connection and +everything involved is probably already closed down. diff --git a/flask.py b/flask.py index 00d23287..f5d94fbe 100644 --- a/flask.py +++ b/flask.py @@ -163,8 +163,10 @@ class _RequestContext(object): def __exit__(self, exc_type, exc_value, tb): # do not pop the request stack if we are in debug mode and an # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. - if tb is None or not self.app.debug: + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + if not self.request.environ.get('flask._preserve_context') and \ + (tb is None or not self.app.debug): self.pop() @@ -1021,9 +1023,40 @@ class Flask(_PackageBoundObject): def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?foo=42') + assert request.args['foo'] == '42' + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. """ from werkzeug import Client - return Client(self, self.response_class, use_cookies=True) + class FlaskClient(Client): + preserve_context = context_preserved = False + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + def __enter__(self): + self.preserve_context = True + return self + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() + return FlaskClient(self, self.response_class, use_cookies=True) def open_session(self, request): """Creates or opens a new session. Default implementation stores all diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3f9588bc..6fbc88a6 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -58,6 +58,7 @@ class ContextTestCase(unittest.TestCase): assert index() == 'Hello World!' with app.test_request_context('/meh'): assert meh() == 'http://localhost/meh' + assert flask._request_ctx_stack.top is None def test_manual_context_binding(self): app = flask.Flask(__name__) @@ -76,6 +77,36 @@ class ContextTestCase(unittest.TestCase): else: assert 0, 'expected runtime error' + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + assert flask.g.value == 42 + assert resp.data == 'Hello World!' + assert resp.status_code == 200 + + resp = c.get('/other') + assert not hasattr(flask.g, 'value') + assert 'Internal Server Error' in resp.data + assert resp.status_code == 500 + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + class BasicFunctionalityTestCase(unittest.TestCase):