From 9bed20c07c40a163077b936f740a4e96a6213688 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:29:00 +0100 Subject: [PATCH] Added documentation for appcontext and teardown handlers --- docs/api.rst | 14 +++++- docs/appcontext.rst | 88 +++++++++++++++++++++++++++++++++ docs/contents.rst.inc | 1 + docs/extensiondev.rst | 100 ++++++++++++++++++++++++++------------ docs/reqcontext.rst | 26 ++-------- docs/signals.rst | 23 +++++++++ flask/app.py | 68 +++++++++++++++++++++++--- flask/ctx.py | 18 +++++-- flask/signals.py | 1 + flask/testsuite/appctx.py | 12 +++++ 10 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 docs/appcontext.rst diff --git a/docs/api.rst b/docs/api.rst index 97332870..b09bcad5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -469,8 +469,18 @@ Signals .. 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. + This is always called, even if an error happened. An `exc` keyword + argument is passed with the exception that caused the teardown. + + .. versionchanged:: 0.9 + The `exc` parameter was added. + +.. data:: appcontext_tearing_down + + This signal is sent when the application is tearing down the + application context. This is always called, even if an error happened. + An `exc` keyword argument is passed with the exception that caused the + teardown. .. currentmodule:: None diff --git a/docs/appcontext.rst b/docs/appcontext.rst new file mode 100644 index 00000000..c331ffa5 --- /dev/null +++ b/docs/appcontext.rst @@ -0,0 +1,88 @@ +.. _app_context: + +The Application Context +======================= + +.. versionadded:: 0.9 + +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. + +There is a third state which is sitting in between a little bit. +Sometimes you are dealing with an application in a way that is similar to +how you interact with applications during request handling just that there +is no request active. Consider for instance that you're sitting in an +interactive Python shell and interacting with the application, or a +command line application. + +The application context is what powers the :data:`~flask.current_app` +context local. + +Purpose of the Application Context +---------------------------------- + +The main reason for the application's context existance is that in the +past a bunch of functionality was attached to the request context in lack +of a better solution. Since one of the pillar's of Flask's design is that +you can have more than one application in the same Python process. + +So how does the code find the “right” application? In the past we +recommended passing applications around explicitly, but that caused issues +with libraries that were not designed with that in mind for libraries for +which it was too inconvenient to make this work. + +A common workaround for that problem was to use the +:data:`~flask.current_app` proxy later on, which was bound to the current +request's application reference. Since however creating such a request +context is an unnecessarily expensive operation in case there is no +request around, the application context was introduced. + +Creating an Application Context +------------------------------- + +To make an application context there are two ways. The first one is the +implicit one: whenever a request context is pushed, an application context +will be created alongside if this is necessary. As a result of that, you +can ignore the existance of the application context unless you need it. + +The second way is the explicit way using the +:meth:`~flask.Flask.app_context` method:: + + from flask import Flask, current_app + + app = Flask(__name__) + with app.app_context(): + # within this block, current_app points to app. + print current_app.name + +The application context is also used by the :func:`~flask.url_for` +function in case a ``SERVER_NAME`` was configured. This allows you to +generate URLs even in the absence of a request. + +Locality of the Context +----------------------- + +The application context is created and destroyed as necessary. It never +moves between threads and it will not be shared between requests. As such +it is the perfect place to store database connection information and other +things. The internal stack object is called :data:`flask._app_ctx_stack`. +Extensions are free to store additional information on the topmost level, +assuming they pick a sufficiently unique name. + +For more information about that, see :ref:`extension-dev`. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index a8ebc0d7..a1893c48 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -18,6 +18,7 @@ instructions for web development with Flask. config signals views + appcontext reqcontext blueprints extensions diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 60aa6c37..2511cec7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -1,3 +1,5 @@ +.. _extension-dev: + Flask Extension Development =========================== @@ -152,6 +154,11 @@ What to use depends on what you have in mind. For the SQLite 3 extension we will use the class-based approach because it will provide users with an object that handles opening and closing database connections. +What's important about classes is that they encourage to be shared around +on module level. In that case, the object itself must not under any +circumstances store any application specific state and must be shareable +between different application. + The Extension Code ------------------ @@ -159,7 +166,13 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: import sqlite3 - from flask import _request_ctx_stack + # Find the stack on which we want to store the database connection. + # Starting with Flask 0.9, the _app_ctx_stack is the correct one, + # before that we need to use the _request_ctx_stack. + try: + from flask import _app_ctx_stack as stack + except ImportError: + from flask import _request_ctx_stack as stack class SQLite3(object): @@ -172,26 +185,28 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: self.app = None def init_app(self, app): - self.app = app - self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.teardown_request(self.teardown_request) - self.app.before_request(self.before_request) + app.config.setdefault('SQLITE3_DATABASE', ':memory:') + # Use the newstyle teardown_appcontext if it's available, + # otherwise fall back to the request context + if hasattr(app, 'teardown_appcontext'): + app.teardown_appcontext(self.teardown) + else: + app.teardown_request(self.teardown) def connect(self): return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) - def before_request(self): - ctx = _request_ctx_stack.top - ctx.sqlite3_db = self.connect() - - def teardown_request(self, exception): - ctx = _request_ctx_stack.top - ctx.sqlite3_db.close() + def teardown(self, exception): + ctx = stack.top + if hasattr(ctx, 'sqlite3_db'): + ctx.sqlite3_db.close() @property def connection(self): - ctx = _request_ctx_stack.top + ctx = stack.top if ctx is not None: + if not hasattr(ctx, 'sqlite3_db'): + ctx.sqlite3_db = self.connect() return ctx.sqlite3_db @@ -204,14 +219,19 @@ So here's what these lines of code do: factory pattern for creating applications. The ``init_app`` will set the configuration for the database, defaulting to an in memory database if no configuration is supplied. In addition, the ``init_app`` method attaches - ``before_request`` and ``teardown_request`` handlers. + the ``teardown`` handler. It will try to use the newstyle app context + handler and if it does not exist, falls back to the request context + one. 3. Next, we define a ``connect`` method 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 - ``_request_ctx_stack.top``. Extensions should use the top context and not the - ``g`` object to store things like database connections. -5. Finally, we add a ``connection`` property that simplifies access to the context's - database. +4. Finally, we add a ``connection`` property that on first access opens + the database connection and stores it on the context. + + Note here that we're attaching our database connection to the top + application context via ``_app_ctx_stack.top``. Extensions should use + the top context for storing their own information with a sufficiently + complex name. Note that we're falling back to the + ``_request_ctx_stack.top`` if the application is using an older + version of Flask that does not support it. So why did we decide on a class-based approach here? Because using our extension looks something like this:: @@ -241,19 +261,38 @@ for creating apps:: Keep in mind that supporting this factory pattern for creating apps is required for approved flask extensions (described below). +.. admonition:: Note on ``init_app`` -Using _request_ctx_stack ------------------------- + As you noticed, ``init_app`` does not assign ``app`` to ``self``. This + is intentional! Class based Flask extensions must only store the + application on the object when the application was passed to the + constructor. This tells the extension: I am not interested in using + multiple applications. -In the example above, before every request, a ``sqlite3_db`` variable is assigned -to ``_request_ctx_stack.top``. In a view function, this variable is accessible -using the ``connection`` property of ``SQLite3``. During the teardown of a -request, the ``sqlite3_db`` connection is closed. By using this pattern, the -*same* connection to the sqlite3 database is accessible to anything that needs it -for the duration of the request. + When the extension needs to find the current application and it does + not have a reference to it, it must either use the + :data:`~flask.current_app` context local or change the API in a way + that you can pass the application explicitly. -End-Of-Request Behavior ------------------------ + +Using _app_ctx_stack +-------------------- + +In the example above, before every request, a ``sqlite3_db`` variable is +assigned to ``_app_ctx_stack.top``. In a view function, this variable is +accessible using the ``connection`` property of ``SQLite3``. During the +teardown of a request, the ``sqlite3_db`` connection is closed. By using +this pattern, the *same* connection to the sqlite3 database is accessible +to anything that needs it for the duration of the request. + +If the :data:`~flask._app_ctx_stack` does not exist because the user uses +an old version of Flask, it is recommended to fall back to +:data:`~flask._request_ctx_stack` which is bound to a request. + +Teardown Behavior +----------------- + +*This is only relevant if you want to support Flask 0.6 and older* 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 @@ -270,7 +309,6 @@ pattern is a good way to support both:: 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 diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 0249b88e..327afe6c 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -6,27 +6,7 @@ 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”. +It is recommended that you read the :api:`app-context` chapter first. Diving into Context Locals -------------------------- @@ -107,6 +87,10 @@ 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. +Another thing of note is that the request context will automatically also +create an :ref:`application context ` when it's pushed and +there is no application context for that application so far. + .. _callbacks-and-errors: Callbacks and Errors diff --git a/docs/signals.rst b/docs/signals.rst index a4cd4157..959c53bd 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -268,4 +268,27 @@ The following signals exist in Flask: from flask import request_tearing_down request_tearing_down.connect(close_db_connection, app) + As of Flask 0.9, this will also be passed an `exc` keyword argument + that has a reference to the exception that caused the teardown if + there was one. + +.. data:: flask.appcontext_tearing_down + :noindex: + + This signal is sent when the app context 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, **extra): + session.close() + + from flask import request_tearing_down + appcontext_tearing_down.connect(close_db_connection, app) + + This will also be passed an `exc` keyword argument that has a reference + to the exception that caused the teardown if there was one. + .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index a0ffed66..8460f476 100644 --- a/flask/app.py +++ b/flask/app.py @@ -35,7 +35,7 @@ from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ - request_tearing_down + request_tearing_down, appcontext_tearing_down # a lock used for logger initialization _logger_lock = Lock() @@ -364,6 +364,14 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 self.teardown_request_funcs = {} + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs = [] + #: A dictionary with lists of functions that can be used as URL #: value processor functions. Whenever a URL is built these functions #: are called to modify the dictionary of values in place. The key @@ -1106,10 +1114,42 @@ class Flask(_PackageBoundObject): 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. + + When a teardown function was called because of a exception it will + be passed an error object. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f + @setupmethod + def teardown_appcontext(self, f): + """Registers a function to be called when the application context + ends. These functions are typically also called when the request + context is popped. + + Example:: + + ctx = app.app_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the app context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Since a request context typically also manages an application + context it would also be called when you pop a request context. + + When a teardown function was called because of an exception it will + be passed an error object. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + @setupmethod def context_processor(self, f): """Registers a template context processor function.""" @@ -1485,23 +1525,39 @@ class Flask(_PackageBoundObject): self.save_session(ctx.session, response) return response - def do_teardown_request(self): + def do_teardown_request(self, exc=None): """Called after the actual request dispatching and will 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. + + .. versionchanged:: 0.9 + Added the `exc` argument. Previously this was always using the + current exception information. """ + if exc is None: + exc = sys.exc_info()[1] funcs = reversed(self.teardown_request_funcs.get(None, ())) bp = _request_ctx_stack.top.request.blueprint if bp is not None and bp in self.teardown_request_funcs: funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) - exc = sys.exc_info()[1] for func in funcs: rv = func(exc) - if rv is not None: - return rv - request_tearing_down.send(self) + request_tearing_down.send(self, exc=exc) + + def do_teardown_appcontext(self, exc=None): + """Called when an application context is popped. This works pretty + much the same as :meth:`do_teardown_request` but for the application + context. + + .. versionadded:: 0.9 + """ + if exc is None: + exc = sys.exc_info()[1] + for func in reversed(self.teardown_appcontext_funcs): + func(exc) + appcontext_tearing_down.send(self, exc=exc) def app_context(self): """Binds the application only. For as long as the application is bound diff --git a/flask/ctx.py b/flask/ctx.py index 887b2598..0ed5ea43 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +import sys + from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack, _app_ctx_stack @@ -86,8 +88,11 @@ class AppContext(object): """Binds the app context to the current context.""" _app_ctx_stack.push(self) - def pop(self): + def pop(self, exc=None): """Pops the app context.""" + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) rv = _app_ctx_stack.pop() assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ % (rv, self) @@ -197,13 +202,18 @@ class RequestContext(object): if self.session is None: self.session = self.app.make_null_session() - def pop(self): + def pop(self, exc=None): """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. + + .. versionchanged:: 0.9 + Added the `exc` argument. """ self.preserved = False - self.app.do_teardown_request() + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) rv = _request_ctx_stack.pop() assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ % (rv, self) @@ -231,7 +241,7 @@ class RequestContext(object): (tb is not None and self.app.preserve_context_on_exception): self.preserved = True else: - self.pop() + self.pop(exc_value) def __repr__(self): return '<%s \'%s\' [%s] of %s>' % ( diff --git a/flask/signals.py b/flask/signals.py index eeb763d4..78a77bd5 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -49,3 +49,4 @@ 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') +appcontext_tearing_down = _signals.signal('appcontext-tearing-down') diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index c60dbc67..a4ad479b 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -53,6 +53,18 @@ class AppContextTestCase(FlaskTestCase): self.assert_equal(flask.current_app._get_current_object(), app) self.assert_equal(flask._app_ctx_stack.top, None) + def test_app_tearing_down(self): + cleanup_stuff = [] + app = flask.Flask(__name__) + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + with app.app_context(): + pass + + self.assert_equal(cleanup_stuff, [None]) + def suite(): suite = unittest.TestSuite()