20170402

Creating an Intermediate Flask Application (1/6)

A Basic Intermediate Flask App

This is the first in a series of blog posts that will cover what I have learned about developing Web application sin Python using Flask. Disclaimer: I am not an expert! These posts are from the point of view of someone new to Flask but not Web development. This is what worked for me.

Before I dive in, here's a little background for those who aren't familiar with what Flask is.

Flask is a microframework for developing Web applications. It doesn't have all the bells and whistles of a Django or Ruby on Rails; it just includes the basics. It provides an API for request routing, sessions, templating, etc. but doesn't include a way to generate/validate forms, an MVC scaffolding or ORM out of the box.

That isn't to say that you can't get these features under Flask. In fact, Flask has a number of extensions that can provide many of the features you get with a more heavyweight solution. But they are optional.

None of this information is new. It's culled from disparate sources I found on the Web when learning Flask. I recommend the following, all of which I learned a great deal from:

1. Setting up virtualenv

Where to begin? Let's create a new repo on github and then clone it locally. I'll call mine flask-example.

$ git clone git@github.com:christopherdeutsch/flask-example.git
$ cd flask-example

Next use virtualenv to create a Python virtual environment. This will allow us to collect all of the Python modules we'll be using, and their versions, in one place. virtualenv creates a directory that will contain all the files, usually called something like env.

$ virtualenv env
New python executable in /Users/cdeutsch/src/flask-example/env/bin/python2.7
Also creating executable in /Users/cdeutsch/src/flask-example/env/bin/python
Please make sure you remove any previous custom paths from your /Users/cdeutsch/.pydistutils.cfg file.
Installing setuptools, pip, wheel...done.


You'll need to activate the virtualenv in order to work on your project. This performs some magic under the hood so that your Python commands will be routed to the virtualenv (it modifies your PATH environment variable).

$ source env/bin/activate
(env) $

See the "(env)" in front of the prompt? That's how you know you're using the virtualenv called env.

Now let's install the Flask python module into our virtualenv.

(env) $ pip install flask
Collecting flask
  Downloading Flask-0.12.1-py2.py3-none-any.whl (82kB)
    100% |████████████████████████████████| 92kB 675kB/s
Collecting Jinja2>=2.4 (from flask)
  Using cached Jinja2-2.9.5-py2.py3-none-any.whl
Collecting Werkzeug>=0.7 (from flask)
  Using cached Werkzeug-0.12.1-py2.py3-none-any.whl
Collecting click>=2.0 (from flask)
  Using cached click-6.7-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
Installing collected packages: MarkupSafe, Jinja2, Werkzeug, click, itsdangerous, flask
Successfully installed Jinja2-2.9.5 MarkupSafe-1.0 Werkzeug-0.12.1 click-6.7 flask-0.12.1 itsdangerous-0.24


Finally, we need to save a list of the Python modules we need and their versions. We'll store them in a file called requirements.txt.

(env) $ pip freeze > requirements.txtIf you look inside you'll just see a list of modules and versions.

(env) $ cat requirements.txt
appdirs==1.4.3
click==6.7
Flask==0.12.1
itsdangerous==0.24
Jinja2==2.9.5
MarkupSafe==1.0
packaging==16.8
pyparsing==2.2.0
six==1.10.0
Werkzeug==0.12.1


It bears mentioning that this is a very important step. When you check out the source and start working on it, you always want to make sure that you're using the same module versions that the code was developed and tested with. This is especially important when other people need to work on your code.

(Sidenote: you can install all the python modules from a requirements.txt file by running pip install -r requirements.txt. Make sure that you activate the virtualenv before installing!)

2. The Python module

It's good practice to organize your code into a Python module, so let's do that next.

(env) $ mkdir flask_example
(env) $ touch flask_example/__init__.py

3. The application factory

When you're first learning Flask, you'll usually see the application set up something like this:


from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!" 
 
if __name__ == "__main__":
    app.run() 

But when you start to work on anything non-trivial, it's important to use the Flask application factory pattern. All it boils down to is creating your application object inside of a function. In flask_example/__init__.py insert the following:

from flask import Flask

def create_app():
  app = Flask(__name__)



  @app.route("/")
  def hello():
    return "Hello World!" 

  return app

The most important aspect of this is for testing: it will allow you to (re)create your application during tests as necessary, potentially using different configurations. When working on my first app, as my number of and flask extensions grew, this pattern became absolutely necessary.

4. Configuration files

There are a few different ways to do this under Flask, but this is the one I like the most. 

The program needs a way to store it's configuration, including a way to create different configuration files for each environment it will run in (e.g. dev, test, production) and a way to store secrets (passwords, API keys) without checking them into source control.

First create a config module:

(env) $ mkdir config
(env) $ touch config/__init__.py
(env) $ touch config/default.py
(env) $ touch config/dev.py


The default.py will contain settings that are always loaded in any environment; the dev.py file will only be used for local development. Drop the following settings into default.py:

EXCITING_MESSAGE = "Hello World!"
DEBUG = False

And then in dev.py:

DEBUG = True

The EXCITING message will be available in any environment. The DEBUG setting illustrates how to set a configuration parameter to default to a particular value and then override the default for specific environments. In this case, we only want to enable debugging when we're running in dev mode.

Now circle back to flask_example/__init__.py and add configuration file support.

from flask import Flask

def create_app(config_filename='dev'):
  app = Flask(__name__, instance_relative_config=True)
  app.config.from_object('config.default')
  app.config.from_object('config.' + config_filename)

  @app.route("/")
  def hello():
    return app.config['EXCITING_MESSAGE']

  return app


Note that configuration variables can be accessed from the app.configuration dictionary.

Finally, a word on secrets. It goes without saying that you don't want to check your passwords, api keys and such into source control-- that's just asking for trouble. An easy way to avoid this is to place them in config files which are not checked into git. So in the simplest case, you could have a config/production.py file that's in your project's gitignore. Of course, you're still left with the problem of how to distribute these files to developers and how to back them up.

(Aside: I like the idea of encrypting secrets with GPG and then committing the encrypted file to the application source code repo, and then using a script, git hook, or git-crypt to install them. But what I've usually seen on teams is using a password sharing app like LastPass or 1Password to pass the files around.)

5. Running it

In order to actually run our application we'll need to create a small script. Call it app.py:

#!/usr/bin/env python

from flask_example import create_app

app = create_app()
app.run()


Now you can run it:

(env) $ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)


Now if you point a web browser to the above URL you should see "Hello World!".

6. Testing it

At this point we have the opportunity to start putting our tests in place. For my projects I'm using pytest.

(env) $ pip install pytest
[...]
Successfully installed py-1.4.33 pytest-3.0.7


Create a directory to hold the tests. We'll put the tests inside of the module.

(env) $ mkdir flask_example/test

Another option is to create a test directory at the top level of the repo, but I have found it useful to keep the tests with the module. It helps keep things distinct if you add new modules.

Now I'll add a failing test for hello() in flask_example/test/test_hello.py:

import unittest
import flask_example

class HelloTestCase(unittest.TestCase):
  def setUp(self):
    self.app = flask_example.create_app()
    self.app.config['TESTING'] = True
    self.client = self.app.test_client()

  def test_hello(self):
    rv = self.client.get('/')
    self.assertEquals(rv.data, "failure")

if __name__ == '__main__':
    unittest.main()



And run it:

(env) $ python -m pytest -q flask_example/test/test_hello.py
[...]
self =

    def test_hello(self):
      rv = self.client.get('/')
>     self.assertEquals(rv.data, "failure")
E     AssertionError: 'Hello World!' != 'failure'

flask_example/test/test_hello.py:12: AssertionError
1 failed in 0.15 seconds

To see the test pass, change "failure" to "Hello World!":
(env) $ python -m pytest -q flask_example/test/test_hello.py
1 passed in 0.10 seconds


So what's happening here?

The setUp() method is called before each test is run. In this case, that means that every time we run a test a new copy of the application will be created, using the application factory method create_app().

The test_client() creates a client we can use to query our Flask routes. It can be used for a simple get of a URL or to post form data.

Something I have found useful is to keep a separate testing config file that sets TESTING=True, disables sending email, etc. Using the factory app pattern this is as simple as using self.app = flask_example.create_app('unittest') from setUp().

That's all for now. Next up I'll look at blueprints, structuring the project to follow the MVC pattern, and using templates.