Skip to content

Commit c631613

Browse files
committed
Context preserving is now part of Flask and not the test client. This fixes pallets#326
1 parent d2eefe2 commit c631613

File tree

5 files changed

+65
-22
lines changed

5 files changed

+65
-22
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Relase date to be decided, codename to be chosen.
4646
- HEAD requests to a method view now automatically dispatch to the `get`
4747
method if no handler was implemented.
4848
- Implemented the virtual :mod:`flask.ext` package to import extensions from.
49+
- The context preservation on exceptions is now an integral component of
50+
Flask itself and no longer of the test client. This cleaned up some
51+
internal logic and lowers the odds of runaway request contexts in unittests.
4952

5053
Version 0.7.3
5154
-------------

flask/ctx.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ def __init__(self, app, environ):
8989
self.flashes = None
9090
self.session = None
9191

92+
# indicator if the context was preserved. Next time another context
93+
# is pushed the preserved context is popped.
94+
self.preserved = False
95+
9296
self.match_request()
9397

9498
# XXX: Support for deprecated functionality. This is doing away with
@@ -114,6 +118,18 @@ def match_request(self):
114118

115119
def push(self):
116120
"""Binds the request context to the current context."""
121+
# If an exception ocurrs in debug mode or if context preservation is
122+
# activated under exception situations exactly one context stays
123+
# on the stack. The rationale is that you want to access that
124+
# information under debug situations. However if someone forgets to
125+
# pop that context again we want to make sure that on the next push
126+
# it's invalidated otherwise we run at risk that something leaks
127+
# memory. This is usually only a problem in testsuite since this
128+
# functionality is not active in production environments.
129+
top = _request_ctx_stack.top
130+
if top is not None and top.preserved:
131+
top.pop()
132+
117133
_request_ctx_stack.push(self)
118134

119135
# Open the session at the moment that the request context is
@@ -128,8 +144,11 @@ def pop(self):
128144
also trigger the execution of functions registered by the
129145
:meth:`~flask.Flask.teardown_request` decorator.
130146
"""
147+
self.preserved = False
131148
self.app.do_teardown_request()
132-
_request_ctx_stack.pop()
149+
rv = _request_ctx_stack.pop()
150+
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
151+
% (rv, self)
133152

134153
def __enter__(self):
135154
self.push()
@@ -141,6 +160,16 @@ def __exit__(self, exc_type, exc_value, tb):
141160
# access the request object in the interactive shell. Furthermore
142161
# the context can be force kept alive for the test client.
143162
# See flask.testing for how this works.
144-
if not self.request.environ.get('flask._preserve_context') and \
145-
(tb is None or not self.app.preserve_context_on_exception):
163+
if self.request.environ.get('flask._preserve_context') or \
164+
(tb is not None and self.app.preserve_context_on_exception):
165+
self.preserved = True
166+
else:
146167
self.pop()
168+
169+
def __repr__(self):
170+
return '<%s \'%s\' [%s] of %s>' % (
171+
self.__class__.__name__,
172+
self.request.url,
173+
self.request.method,
174+
self.app.name
175+
)

flask/globals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def _lookup_object(name):
1919
raise RuntimeError('working outside of request context')
2020
return getattr(top, name)
2121

22+
2223
# context locals
2324
_request_ctx_stack = LocalStack()
2425
current_app = LocalProxy(partial(_lookup_object, 'app'))

flask/testing.py

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class FlaskClient(Client):
3737
Basic usage is outlined in the :ref:`testing` chapter.
3838
"""
3939

40-
preserve_context = context_preserved = False
40+
preserve_context = False
4141

4242
@contextmanager
4343
def session_transaction(self, *args, **kwargs):
@@ -88,7 +88,6 @@ def session_transaction(self, *args, **kwargs):
8888
self.cookie_jar.extract_wsgi(c.request.environ, headers)
8989

9090
def open(self, *args, **kwargs):
91-
self._pop_reqctx_if_necessary()
9291
kwargs.setdefault('environ_overrides', {}) \
9392
['flask._preserve_context'] = self.preserve_context
9493

@@ -97,14 +96,10 @@ def open(self, *args, **kwargs):
9796
follow_redirects = kwargs.pop('follow_redirects', False)
9897
builder = make_test_environ_builder(self.application, *args, **kwargs)
9998

100-
old = _request_ctx_stack.top
101-
try:
102-
return Client.open(self, builder,
103-
as_tuple=as_tuple,
104-
buffered=buffered,
105-
follow_redirects=follow_redirects)
106-
finally:
107-
self.context_preserved = _request_ctx_stack.top is not old
99+
return Client.open(self, builder,
100+
as_tuple=as_tuple,
101+
buffered=buffered,
102+
follow_redirects=follow_redirects)
108103

109104
def __enter__(self):
110105
if self.preserve_context:
@@ -114,12 +109,10 @@ def __enter__(self):
114109

115110
def __exit__(self, exc_type, exc_value, tb):
116111
self.preserve_context = False
117-
self._pop_reqctx_if_necessary()
118-
119-
def _pop_reqctx_if_necessary(self):
120-
if self.context_preserved:
121-
# we have to use _request_ctx_stack.top.pop instead of
122-
# _request_ctx_stack.pop since we want teardown handlers
123-
# to be executed.
124-
_request_ctx_stack.top.pop()
125-
self.context_preserved = False
112+
113+
# on exit we want to clean up earlier. Normally the request context
114+
# stays preserved until the next request in the same thread comes
115+
# in. See RequestGlobals.push() for the general behavior.
116+
top = _request_ctx_stack.top
117+
if top is not None and top.preserved:
118+
top.pop()

flask/testsuite/basic.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,23 @@ def for_bar_foo():
904904
self.assertEqual(c.get('/bar/').data, 'bar')
905905
self.assertEqual(c.get('/bar/123').data, '123')
906906

907+
def test_preserve_only_once(self):
908+
app = flask.Flask(__name__)
909+
app.debug = True
910+
911+
@app.route('/fail')
912+
def fail_func():
913+
1/0
914+
915+
c = app.test_client()
916+
for x in xrange(3):
917+
with self.assert_raises(ZeroDivisionError):
918+
c.get('/fail')
919+
920+
self.assert_(flask._request_ctx_stack.top is not None)
921+
flask._request_ctx_stack.pop()
922+
self.assert_(flask._request_ctx_stack.top is None)
923+
907924

908925
class ContextTestCase(FlaskTestCase):
909926

0 commit comments

Comments
 (0)