From 05161d35844ba7cc1f46c08a51de632b5f3c5269 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 1 Sep 2013 01:32:41 +0600 Subject: [PATCH] Rewrote tutorial to use the g based appcontext object --- docs/index.rst | 1 + docs/tutorial/dbcon.rst | 106 ++++++++++++++++++++++---------------- docs/tutorial/dbinit.rst | 49 +++++++++--------- docs/tutorial/index.rst | 2 +- docs/tutorial/setup.rst | 70 ++++++++++++++----------- docs/tutorial/views.rst | 16 +++--- examples/flaskr/flaskr.py | 44 +++++++++------- 7 files changed, 162 insertions(+), 126 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ef57e07c..43702409 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ following links: - `Jinja2 Documentation `_ - `Werkzeug Documentation `_ + .. _Jinja2: http://jinja.pocoo.org/2/ .. _Werkzeug: http://werkzeug.pocoo.org/ diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 837983e4..55526fbf 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -1,56 +1,72 @@ .. _tutorial-dbcon: -Step 4: Request Database Connections ------------------------------------- - -Now we know how we can open database connections and use them for scripts, -but how can we elegantly do that for requests? We will need the database -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:: - - @app.before_request - def before_request(): - g.db = connect_db() - - @app.teardown_request - def teardown_request(exception): - db = getattr(g, 'db', None) - if db is not None: - db.close() - -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 -passed the response that will be sent to the client. They have to return -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. - -We store our current database connection on the special :data:`~flask.g` -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 -the scenes to ensure it does the right thing. - -For an even better way to handle such resources see the :ref:`sqlite3` -documentation. - -Continue to :ref:`tutorial-views`. +Step 3: Database Connections +---------------------------- + +We have created a function for establishing a database connection with +`create_db` but by itself that's not particularly useful. Creating and +closing database connections all the time is very inefficient, so we want +to keep it around for longer. Because database connections encapsulate a +transaction we also need to make sure that only one request at the time +uses the connection. So how can we elegantly do that with Flask? + +This is where the application context comes into play. So let's start +there. + +Flask provides us with two contexts: the application context and the +request context. For the time being all you have to know is that there +are special variables that use these. For instance the +:data:`~flask.request` variable is the request object associated with +the current request, whereas :data:`~flask.g` is a general purpose +variable associated with the current application context. We will go into +the details of this a bit later. + +For the time being all you have to know is that you can store information +savely on the :data:`~flask.g` object. + +So when do you put it on there? To do that you can make a helper +function. The first time the function is called it will create a database +connection for the current context and successive calls will return the +already established connection:: + + def get_db(): + """Opens a new database connection if there is none yet for the + current application context. + """ + if not hasattr(g, 'sqlite_db'): + g.sqlite_db = connect_db() + return g.sqlite_db + + +So now we know how to connect, but how do we properly disconnect? For +that flask provides us with the :meth:`~flask.Flask.teardown_appcontext` +decorator. It's executed every time the application context tears down:: + + @app.teardown_appcontext + def close_db(error): + """Closes the database again at the end of the request.""" + if hasattr(g, 'sqlite_db'): + g.sqlite_db.close() + +Functions marked with :meth:`~flask.Flask.teardown_appcontext` are called +every time the app context tears down. So what does this mean? +Essentially the app context is created before the request comes in and is +destroyed (teared down) whenever the request finishes. A teardown can +happen because of two reasons: either everything went well (the error +parameter will be `None`) or an exception happend in which case the error +is passed to the teardown function. + +Curious about what these contexts mean? Have a look at the +:ref:`app-context` documentation to learn more. + +Continue to :ref:`tutorial-dbinit`. .. hint:: Where do I put this code? If you've been following along in this tutorial, you might be wondering where to put the code from this step and the next. A logical place is to group these module-level functions together, and put your new - ``before_request`` and ``teardown_request`` functions below your existing + ``get_db`` and ``close_db`` functions below your existing ``init_db`` function (following the tutorial line-by-line). If you need a moment to find your bearings, take a look at how the `example diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 1241193a..cd128159 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -1,6 +1,6 @@ .. _tutorial-dbinit: -Step 3: Creating The Database +Step 4: Creating The Database ============================= Flaskr is a database powered application as outlined earlier, and more @@ -20,36 +20,39 @@ to provide the path to the database there which leaves some place for errors. It's a good idea to add a function that initializes the database for you to the application. -If you want to do that, you first have to import the -:func:`contextlib.closing` function from the contextlib package. -Accordingly, add the following lines to your existing imports in `flaskr.py`:: - - from contextlib import closing - -Next we can create a function called `init_db` that initializes the -database. For this we can use the `connect_db` function we defined -earlier. Just add that function below the `connect_db` function in -`flaskr.py`:: +To do this we can create a function called `init_db` that initializes the +database. Let me show you the code first. Just add that function below +the `connect_db` function in `flaskr.py`:: def init_db(): - with closing(connect_db()) as db: + app app.app_context(): + db = get_db() with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() -The :func:`~contextlib.closing` helper function allows us to keep a -connection open for the duration of the `with` block. The -:func:`~flask.Flask.open_resource` method of the application object -supports that functionality out of the box, so it can be used in the -`with` block directly. This function opens a file from the resource +So what's happening here? Remember how we learned last chapter that the +application context is created every time a request comes in? Here we +don't have a request yet, so we need to create the application context by +hand. Without an application context the :data:`~flask.g` object does not +know yet to which application it becomes as there could be more than one! + +The ``with app.app_context()`` statement establishes the application +context for us. In the body of the with statement the :flask:`~flask.g` +object will be associated with ``app``. At the end of the with statement +the association is released and all teardown functions are executed. This +means that our database connection is disconnected after the commit. + +The :func:`~flask.Flask.open_resource` method of the application object +is a convenient helper function that will open a resource that the +application provides. This function opens a file from the resource location (your `flaskr` folder) and allows you to read from it. We are using this here to execute a script on the database connection. -When we connect to a database we get a connection object (here called -`db`) that can give us a cursor. On that cursor there is a method to -execute a complete script. Finally we only have to commit the changes. -SQLite 3 and other transactional databases will not commit unless you -explicitly tell it to. +The connection object provided by SQLite can give us a cursor object. +On that cursor there is a method to execute a complete script. Finally we +only have to commit the changes. SQLite 3 and other transactional +databases will not commit unless you explicitly tell it to. Now it is possible to create a database by starting up a Python shell and importing and calling that function:: @@ -63,4 +66,4 @@ importing and calling that function:: you did call the `init_db` function and that your table names are correct (singular vs. plural for example). -Continue with :ref:`tutorial-dbcon` +Continue with :ref:`tutorial-views` diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 3f2d659e..da37cf7a 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -24,8 +24,8 @@ the `example source`_. folders schema setup - dbinit dbcon + dbinit views templates css diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 4c5d1e49..5b059c56 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -5,31 +5,18 @@ Step 2: Application Setup Code Now that we have the schema in place we can create the application module. Let's call it `flaskr.py` inside the `flaskr` folder. For starters we -will add the imports we will need as well as the config section. For -small applications it's a possibility to drop the configuration directly -into the module which we will be doing here. However a cleaner solution -would be to create a separate `.ini` or `.py` file and load that or import -the values from there. +will add the imports and create the application object. For small +applications it's a possibility to drop the configuration directly into +the module which we will be doing here. However a cleaner solution would +be to create a separate `.ini` or `.py` file and load that or import the +values from there. -In `flaskr.py`:: +First we add the imports in `flaskr.py`:: # all the imports import sqlite3 - from flask import Flask, request, session, g, redirect, url_for, \ - abort, render_template, flash - - # configuration - DATABASE = '/tmp/flaskr.db' - DEBUG = True - SECRET_KEY = 'development key' - USERNAME = 'admin' - PASSWORD = 'default' - -.. admonition:: Windows - - If you are on Windows, replace `/tmp/flaskr.db` with a different writeable - path of your choice, in the configuration and for the rest of this - tutorial. + from flask import Flask, request, session, g, redirect, url_for, abort, \ + render_template, flash Next we can create our actual application and initialize it with the config from the same file, in `flaskr.py`:: @@ -38,10 +25,24 @@ config from the same file, in `flaskr.py`:: app = Flask(__name__) app.config.from_object(__name__) -:meth:`~flask.Config.from_object` will look at the given object (if it's a -string it will import it) and then look for all uppercase variables -defined there. In our case, the configuration we just wrote a few lines -of code above. You can also move that into a separate file. + # Load default config and override config from an environment variable + app.config.update(dict( + DATABASE='/tmp/flaskr.db', + DEBUG=True, + SECRET_KEY='development key', + USERNAME='admin', + PASSWORD='default' + )) + app.config.from_envvar('FLASKR_SETTINGS', silent=True) + +The :class:`~flask.Config` object works similar to a dictionary so we +can update it with new values. + +.. admonition:: Windows + + If you are on Windows, replace `/tmp/flaskr.db` with a different writeable + path of your choice, in the configuration and for the rest of this + tutorial. Usually, it is a good idea to load a separate, environment specific configuration file. Flask allows you to import multiple configurations and it @@ -54,7 +55,12 @@ Simply define the environment variable :envvar:`FLASKR_SETTINGS` that points to a config file to be loaded. The silent switch just tells Flask to not complain if no such environment key is set. -The `secret_key` is needed to keep the client-side sessions secure. +In addition to that you can use the :meth:`~flask.Config.from_object` +method on the config object and provide it with an import name of a +module. Flask will the initialize the variable from that module. Note +that in all cases only variable names that are uppercase are considered. + +The ``SECRET_KEY`` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The debug flag enables or disables the interactive debugger. *Never leave debug mode activated in a production system*, because it will allow users to @@ -62,12 +68,18 @@ execute code on the server! We also add a method to easily connect to the database specified. That can be used to open a connection on request and also from the interactive -Python shell or a script. This will come in handy later. +Python shell or a script. This will come in handy later. We create a +simple database connection through SQLite and then tell it to use the +:class:`sqlite3.Row` object to represent rows. This allows us to treat +the rows as if they were dictionaries instead of tuples. :: def connect_db(): - return sqlite3.connect(app.config['DATABASE']) + """Connects to the specific database.""" + rv = sqlite3.connect(app.config['DATABASE']) + rv.row_factory = sqlite3.Row + return rv Finally we just add a line to the bottom of the file that fires up the server if we want to run that file as a standalone application:: @@ -93,4 +105,4 @@ focus on that a little later. First we should get the database working. :ref:`externally visible server ` section for more information. -Continue with :ref:`tutorial-dbinit`. +Continue with :ref:`tutorial-dbcon`. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 93bec3bf..69c1c161 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -12,18 +12,17 @@ Show Entries This view shows all the entries stored in the database. It listens on the root of the application and will select title and text from the database. The one with the highest id (the newest entry) will be on top. The rows -returned from the cursor are tuples with the columns ordered like specified -in the select statement. This is good enough for small applications like -here, but you might want to convert them into a dict. If you are -interested in how to do that, check out the :ref:`easy-querying` example. +returned from the cursor look a bit like tuples because we are using +the :class:`sqlite3.Row` row factory. The view function will pass the entries as dicts to the `show_entries.html` template and return the rendered one:: @app.route('/') def show_entries(): - cur = g.db.execute('select title, text from entries order by id desc') - entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + db = get_db() + cur = db.execute('select title, text from entries order by id desc') + entries = cur.fetchall() return render_template('show_entries.html', entries=entries) Add New Entry @@ -39,9 +38,10 @@ redirect back to the `show_entries` page:: def add_entry(): if not session.get('logged_in'): abort(401) - g.db.execute('insert into entries (title, text) values (?, ?)', + db = get_db() + db.execute('insert into entries (title, text) values (?, ?)', [request.form['title'], request.form['text']]) - g.db.commit() + db.commit() flash('New entry was successfully posted') return redirect(url_for('show_entries')) diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 04645efa..6c613279 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -11,22 +11,31 @@ """ from sqlite3 import dbapi2 as sqlite3 -from flask import Flask, request, session, redirect, url_for, abort, \ - render_template, flash, _app_ctx_stack +from flask import Flask, request, session, g, redirect, url_for, abort, \ + render_template, flash -# configuration -DATABASE = '/tmp/flaskr.db' -DEBUG = True -SECRET_KEY = 'development key' -USERNAME = 'admin' -PASSWORD = 'default' # create our little application :) app = Flask(__name__) -app.config.from_object(__name__) + +# Load default config and override config from an environment variable +app.config.update(dict( + DATABASE='/tmp/flaskr.db', + DEBUG=True, + SECRET_KEY='development key', + USERNAME='admin', + PASSWORD='default' +)) app.config.from_envvar('FLASKR_SETTINGS', silent=True) +def connect_db(): + """Connects to the specific database.""" + rv = sqlite3.connect(app.config['DATABASE']) + rv.row_factory = sqlite3.Row + return rv + + def init_db(): """Creates the database tables.""" with app.app_context(): @@ -40,21 +49,16 @@ def get_db(): """Opens a new database connection if there is none yet for the current application context. """ - top = _app_ctx_stack.top - if not hasattr(top, 'sqlite_db'): - sqlite_db = sqlite3.connect(app.config['DATABASE']) - sqlite_db.row_factory = sqlite3.Row - top.sqlite_db = sqlite_db - - return top.sqlite_db + if not hasattr(g, 'sqlite_db'): + g.sqlite_db = connect_db() + return g.sqlite_db @app.teardown_appcontext -def close_db_connection(exception): +def close_db(error): """Closes the database again at the end of the request.""" - top = _app_ctx_stack.top - if hasattr(top, 'sqlite_db'): - top.sqlite_db.close() + if hasattr(g, 'sqlite_db'): + g.sqlite_db.close() @app.route('/')