Skip to content

Commit 04e70bd

Browse files
glyphobetmitsuhiko
authored andcommitted
Add teardown_request decorator. Fixes issue pallets#174
1 parent 3deae1b commit 04e70bd

File tree

4 files changed

+136
-4
lines changed

4 files changed

+136
-4
lines changed

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Release date to be announced, codename to be selected
3737
was incorrectly introduced in 0.6.
3838
- Added `create_jinja_loader` to override the loader creation process.
3939
- Implemented a silent flag for `config.from_pyfile`.
40+
- Added `teardown_request` decorator, for functions that should run at the end
41+
of a request regardless of whether an exception occurred.
4042

4143
Version 0.6.1
4244
-------------

docs/tutorial/dbcon.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ but how can we elegantly do that for requests? We will need the database
88
connection in all our functions so it makes sense to initialize them
99
before each request and shut them down afterwards.
1010

11-
Flask allows us to do that with the :meth:`~flask.Flask.before_request` and
12-
:meth:`~flask.Flask.after_request` decorators::
11+
Flask allows us to do that with the :meth:`~flask.Flask.before_request`,
12+
:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request`
13+
decorators. In debug mode, if an error is raised,
14+
:meth:`~flask.Flask.after_request` won't be run, and you'll have access to the
15+
db connection in the interactive debugger::
1316

1417
@app.before_request
1518
def before_request():
@@ -20,13 +23,29 @@ Flask allows us to do that with the :meth:`~flask.Flask.before_request` and
2023
g.db.close()
2124
return response
2225

26+
If you want to guarantee that the connection is always closed in debug mode, you
27+
can close it in a function decorated with :meth:`~flask.Flask.teardown_request`:
28+
29+
@app.before_request
30+
def before_request():
31+
g.db = connect_db()
32+
33+
@app.teardown_request
34+
def teardown_request(exception):
35+
g.db.close()
36+
2337
Functions marked with :meth:`~flask.Flask.before_request` are called before
24-
a request and passed no arguments, functions marked with
38+
a request and passed no arguments. Functions marked with
2539
:meth:`~flask.Flask.after_request` are called after a request and
2640
passed the response that will be sent to the client. They have to return
2741
that response object or a different one. In this case we just return it
2842
unchanged.
2943

44+
Functions marked with :meth:`~flask.Flask.teardown_request` get called after the
45+
response has been constructed. They are not allowed to modify the request, and
46+
their return values are ignored. If an exception occurred while the request was
47+
being processed, it is passed to each function; otherwise, None is passed in.
48+
3049
We store our current database connection on the special :data:`~flask.g`
3150
object that flask provides for us. This object stores information for one
3251
request only and is available from within each function. Never store such

flask/app.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from __future__ import with_statement
1313

14+
import sys
1415
from threading import Lock
1516
from datetime import timedelta, datetime
1617
from itertools import chain
@@ -247,6 +248,18 @@ def __init__(self, import_name, static_path=None):
247248
#: :meth:`after_request` decorator.
248249
self.after_request_funcs = {}
249250

251+
#: A dictionary with lists of functions that are called after
252+
#: each request, even if an exception has occurred. The key of the
253+
#: dictionary is the name of the module this function is active for,
254+
#: `None` for all requests. These functions are not allowed to modify
255+
#: the request, and their return values are ignored. If an exception
256+
#: occurred while processing the request, it gets passed to each
257+
#: teardown_request function. To register a function here, use the
258+
#: :meth:`teardown_request` decorator.
259+
#:
260+
#: .. versionadded:: 0.7
261+
self.teardown_request_funcs = {}
262+
250263
#: A dictionary with list of functions that are called without argument
251264
#: to populate the template context. The key of the dictionary is the
252265
#: name of the module this function is active for, `None` for all
@@ -704,6 +717,11 @@ def after_request(self, f):
704717
self.after_request_funcs.setdefault(None, []).append(f)
705718
return f
706719

720+
def teardown_request(self, f):
721+
"""Register a function to be run at the end of each request, regardless of whether there was an exception or not."""
722+
self.teardown_request_funcs.setdefault(None, []).append(f)
723+
return f
724+
707725
def context_processor(self, f):
708726
"""Registers a template context processor function."""
709727
self.template_context_processors[None].append(f)
@@ -869,6 +887,20 @@ def process_response(self, response):
869887
response = handler(response)
870888
return response
871889

890+
def do_teardown_request(self):
891+
"""Called after the actual request dispatching and will
892+
call every as :meth:`teardown_request` decorated function.
893+
"""
894+
funcs = reversed(self.teardown_request_funcs.get(None, ()))
895+
mod = request.module
896+
if mod and mod in self.teardown_request_funcs:
897+
funcs = chain(funcs, reversed(self.teardown_request_funcs[mod]))
898+
exc = sys.exc_info()[1]
899+
for func in funcs:
900+
rv = func(exc)
901+
if rv is not None:
902+
return rv
903+
872904
def request_context(self, environ):
873905
"""Creates a request context from the given environment and binds
874906
it to the current context. This must be used in combination with
@@ -947,6 +979,11 @@ def wsgi_app(self, environ, start_response):
947979
even if an exception happens database have the chance to
948980
properly close the connection.
949981
982+
.. versionchanged:: 0.7
983+
The :meth:`teardown_request` functions get called at the very end of
984+
processing the request. If an exception was thrown, it gets passed to
985+
each teardown_request function.
986+
950987
:param environ: a WSGI environment
951988
:param start_response: a callable accepting a status code,
952989
a list of headers and an optional
@@ -965,6 +1002,8 @@ def wsgi_app(self, environ, start_response):
9651002
response = self.process_response(response)
9661003
except Exception, e:
9671004
response = self.make_response(self.handle_exception(e))
1005+
finally:
1006+
self.do_teardown_request()
9681007
request_finished.send(self, response=response)
9691008
return response(environ, start_response)
9701009

tests/flask_tests.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,72 @@ def fails():
413413
assert 'Internal Server Error' in rv.data
414414
assert len(called) == 1
415415

416+
def test_teardown_request_handler(self):
417+
called = []
418+
app = flask.Flask(__name__)
419+
@app.teardown_request
420+
def teardown_request(exc):
421+
called.append(True)
422+
return "Ignored"
423+
@app.route('/')
424+
def root():
425+
return "Response"
426+
rv = app.test_client().get('/')
427+
assert rv.status_code == 200
428+
assert 'Response' in rv.data
429+
assert len(called) == 1
430+
431+
def test_teardown_request_handler_debug_mode(self):
432+
called = []
433+
app = flask.Flask(__name__)
434+
app.debug = True
435+
@app.teardown_request
436+
def teardown_request(exc):
437+
called.append(True)
438+
return "Ignored"
439+
@app.route('/')
440+
def root():
441+
return "Response"
442+
rv = app.test_client().get('/')
443+
assert rv.status_code == 200
444+
assert 'Response' in rv.data
445+
assert len(called) == 1
446+
447+
448+
def test_teardown_request_handler_error(self):
449+
called = []
450+
app = flask.Flask(__name__)
451+
@app.teardown_request
452+
def teardown_request1(exc):
453+
assert type(exc) == ZeroDivisionError
454+
called.append(True)
455+
# This raises a new error and blows away sys.exc_info(), so we can
456+
# test that all teardown_requests get passed the same original
457+
# exception.
458+
try:
459+
raise TypeError
460+
except:
461+
pass
462+
@app.teardown_request
463+
def teardown_request2(exc):
464+
assert type(exc) == ZeroDivisionError
465+
called.append(True)
466+
# This raises a new error and blows away sys.exc_info(), so we can
467+
# test that all teardown_requests get passed the same original
468+
# exception.
469+
try:
470+
raise TypeError
471+
except:
472+
pass
473+
@app.route('/')
474+
def fails():
475+
1/0
476+
rv = app.test_client().get('/')
477+
assert rv.status_code == 500
478+
assert 'Internal Server Error' in rv.data
479+
assert len(called) == 2
480+
481+
416482
def test_before_after_request_order(self):
417483
called = []
418484
app = flask.Flask(__name__)
@@ -430,12 +496,18 @@ def after1(response):
430496
def after2(response):
431497
called.append(3)
432498
return response
499+
@app.teardown_request
500+
def finish1(exc):
501+
called.append(6)
502+
@app.teardown_request
503+
def finish2(exc):
504+
called.append(5)
433505
@app.route('/')
434506
def index():
435507
return '42'
436508
rv = app.test_client().get('/')
437509
assert rv.data == '42'
438-
assert called == [1, 2, 3, 4]
510+
assert called == [1, 2, 3, 4, 5, 6]
439511

440512
def test_error_handling(self):
441513
app = flask.Flask(__name__)

0 commit comments

Comments
 (0)