Large Application Structure

Chapter 7. Large Application Structure

Although having small web applications stored in a single script file can be very convenient, this approach does not scale well. As the application grows in complexity, working with a single large source file becomes problematic.

Unlike most other web frameworks, Flask does not impose a specific organization for large projects; the way to structure the application is left entirely to the developer. In this chapter, a possible way to organize a large application in packages and modules is presented. This structure will be used in the remaining examples of the book.

Project Structure

Example 7-1 shows the basic layout for a Flask application.

Example 7-1. Basic multiple-file Flask application structure

This structure has four top-level folders:

There are also a few new files:

To help you fully understand this structure, the following sections describe the process to convert the hello.py application to it.

Configuration Options

Applications often need several configuration sets. The best example of this is the need to use different databases during development, testing, and production so that they don’t interfere with each other.

Instead of the simple app.config dictionary-like configuration used by hello.py, a hierarchy of configuration classes can be used. Example 7-2 shows the config.py file, with all the settings imported from hello.py.

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
        ['true', 'on', '1']
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
    FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,

    'default': DevelopmentConfig
}

The Config base class contains settings that are common to all configurations; the different subclasses define settings that are specific to a configuration. Additional configurations can be added as needed.

To make configuration more flexible and safe, most settings can be optionally imported from environment variables. For example, the value of the SECRET_KEY, due to its sensitive nature, can be set in the environment, but a default value is provided in case the environment does not define it. Typically, these settings can be used with their defaults during development but should each have an appropriate value set in the corresponding environment variable on the production server. The configuration options for the email server are all imported from environment variables as well, with defaults pointing to the Gmail server for convenience during development.

Tip

Never write passwords or other secrets in a configuration file that is committed to source control.

The SQLALCHEMY_DATABASE_URI variable is assigned different values under each of the three configurations. This enables the application to use a different database in each configuration. This is very important, as you don’t want a run of the unit tests to change the database that you use for day-to-day development. Each configuration tries to import the database URL from an environment variable, and when that is not available it sets a default one based on SQLite. For the testing configuration, the default is an in-memory database, since there is no need to store any data outside of the test run.

The development and production configurations each have a set of mail server configuration options. As an additional way to allow the application to customize its configuration, the Config class and its subclasses can define an init_app() class method that takes the application instance as an argument. For now the base Config class implements an empty init_app() method.

At the bottom of the configuration script, the different configurations are registered in a config dictionary. One of the configurations (the one for development, in this case) is also registered as the default.

Application Package

The application package is where all the application code, templates, and static files live. It is called simply app here, though it can be given an application-specific name if desired. The templates and static directories are now part of the application package, so they are moved inside app. The database models and the email support functions are also moved inside this package, each in its own module, as app/models.py and app/email.py.

Using an Application Factory

The way the application is created in the single-file version is very convenient, but it has one big drawback. Because the application is created in the global scope, there is no way to apply configuration changes dynamically: by the time the script is running, the application instance has already been created, so it is already too late to make configuration changes. This is particularly important for unit tests because sometimes it is necessary to run the application under different configuration settings for better test coverage.

The solution to this problem is to delay the creation of the application by moving it into a factory function that can be explicitly invoked from the script. This not only gives the script time to set the configuration, but also the ability to create multiple application instances—another thing that can be very useful during testing. The application factory function, shown in Example 7-3, is defined in the app package constructor.

Example 7-3. app/__init__.py: application package constructor

This constructor imports most of the Flask extensions currently in use, but because there is no application instance to initialize them with, it creates them uninitialized by passing no arguments into their constructors. The create_app() function is the application factory, which takes as an argument the name of a configuration to use for the application. The configuration settings stored in one of the classes defined in config.py can be imported directly into the application using the from_object() method available in Flask’s app.config configuration object. The configuration object is selected by name from the config dictionary. Once an application is created and configured, the extensions can be initialized. Calling init_app() on the extensions that were created earlier completes their initialization.

The application initialization is now done in this factory function, using the from_object() method from the Flask configuration object, which takes as an argument one of the configuration classes defined in config.py. The init_app() method of the selected configuration is also invoked, to allow more complex initialization procedures to take place.

The factory function returns the created application instance, but note that applications created with the factory function in its current state are incomplete, as they are missing routes and custom error page handlers. This is the topic of the next section.

Implementing Application Functionality in a Blueprint

The conversion to an application factory introduces a complication for routes. In single-script applications, the application instance exists in the global scope, so routes can be easily defined using the app.route decorator. But now that the application is created at runtime, the app.route decorator begins to exist only after create_app() is invoked, which is too late. Custom error page handlers present the same problem, as these are defined with the app.errorhandler decorator.

Luckily, Flask offers a better solution using blueprints. A blueprint is similar to an application in that it can also define routes and error handlers. The difference is that when these are defined in a blueprint they are in a dormant state until the blueprint is registered with an application, at which point they become part of it. Using a blueprint defined in the global scope, the routes and error handlers of the application can be defined in almost the same way as in the single-script application.

Like applications, blueprints can be defined all in a single file or can be created in a more structured way with multiple modules inside a package. To allow for the greatest flexibility, a subpackage inside the application package will be created to host the first blueprint of the application. Example 7-4 shows the package constructor, which creates the blueprint.

Example 7-4. app/main/__init__.py: main blueprint creation

Blueprints are created by instantiating an object of class Blueprint. The constructor for this class takes two required arguments: the blueprint name and the module or package where the blueprint is located. As with applications, Python’s __name__ variable is in most cases the correct value for the second argument.

The routes of the application are stored in an app/main/views.py module inside the package, and the error handlers are in app/main/errors.py. Importing these modules causes the routes and error handlers to be associated with the blueprint. It is important to note that the modules are imported at the bottom of the app/main/ __init__.py script to avoid errors due to circular dependencies. In this particular example the problem is that app/main/views.py and app/main/errors.py in turn are going to import the main blueprint object, so the imports are going to fail unless the circular reference occurs after main is defined.

Tip

The from . import <some-module> syntax is used in Python to represent relative imports. The . in this statement represents the current package. You are going to see another very useful relative import soon that uses the form from .. import <some-module>, where .. represents the parent of the current package.

The blueprint is registered with the application inside the create_app() factory function, as shown in Example 7-5.

Example 7-6 shows the error handlers.

Example 7-6. app/main/errors.py: error handlers in main blueprint

A difference when writing error handlers inside a blueprint is that if the errorhandler decorator is used, the handler will be invoked only for errors that originate in the routes defined by the blueprint. To install application-wide error handlers, the app_errorhandler decorator must be used instead.

Example 7-7 shows the route of the application updated to be in the blueprint.

Example 7-7. app/main/views.py: application routes in main blueprint

There are two main differences when writing a view function inside a blueprint. First, as was done for error handlers earlier, the route decorator comes from the blueprint, so main.route is used instead of app.route. The second difference is in the usage of the url_for() function. As you may recall, the first argument to this function is the endpoint name of the route, which for application-based routes defaults to the name of the view function. For example, in a single-script application the URL for an index() view function can be obtained with url_for('index').

The difference with blueprints is that Flask applies a namespace to all the endpoints defined in a blueprint, so that multiple blueprints can define view functions with the same endpoint names without collisions. The namespace is the name of the blueprint (the first argument to the Blueprint constructor) and is separated from the endpoint name with a dot. The index() view function is then registered with endpoint name main.index and its URL can be obtained with url_for('main.index').

The url_for() function also supports a shorter format for endpoints in blueprints in which the blueprint name is omitted, such as url_for('.index'). With this notation, the blueprint name for the current request is used to complete the endpoint name. This effectively means that redirects within the same blueprint can use the shorter form, while redirects across blueprints must use the fully qualified endpoint name that includes the blueprint name.

To complete the changes to the application package, the form objects are also stored inside the blueprint in the app/main/forms.py module.

Application Script

The flasky.py module in the top-level directory is where the application instance is defined. This script is shown in Example 7-8.

The script begins by creating an application. The configuration is taken from the environment variable FLASK_CONFIG if it’s defined, or else the default configuration is used. Flask-Migrate and the custom context for the Python shell are then initialized.

Because the main script of the application changed from hello.py to flasky.py, the FLASK_APP environment variable needs to be updated accordingly so that the flask command can locate the application instance. It is also useful to enable Flask’s debug mode by setting FLASK_DEBUG=1. For Linux and macOS, this is all done as follows:

And for Microsoft Windows:

Requirements File

It is a good practice for applications to include a requirements.txt file that records all the package dependencies, with the exact version numbers. This is important in case the virtual environment needs to be regenerated on a different machine, such as the machine on which the application will be deployed for production use. This file can be generated automatically by pip with the following command:

It is a good idea to refresh this file whenever a package is installed or upgraded. An example requirements file is shown here:

When you need to build a perfect replica of the virtual environment, you can create a new virtual environment and run the following command on it:

The version numbers in the example requirements.txt file are likely going to be outdated by the time you read this. You can try using more recent releases of the packages, if you like. If you experience any problems, you can always go back to the versions specified here, as those are known to be compatible with the application.

Unit Tests

This application is very small, so there isn’t a lot to test yet. But as an example, two simple tests can be defined, as shown in Example 7-9.

Example 7-9. tests/test_basics.py: unit tests

The tests are written using the standard unittest package from the Python standard library. The setUp() and tearDown() methods of the test case class run before and after each test, and any methods that have a name that begins with test_ are executed as tests.

Tip

If you want to learn more about writing unit tests with Python’s unittest package, read the official documentation.

The setUp() method tries to create an environment for the test that is close to that of a running application. It first creates an application configured for testing and activates its context. This step ensures that tests have access to current_app, like regular requests do. Then it creates a brand-new database for the tests using Flask-SQLAlchemy’s create_all() method. The database and the application context are removed in the tearDown() method.

The first test ensures that the application instance exists. The second test ensures that the application is running under the testing configuration. To make the tests directory a proper package, a tests/ init.py module needs to be added, but this can be an empty file, as the unittest package scans all the modules to discover the tests.

Tip

If you have cloned the application’s Git repository on GitHub, you can run git checkout 7a to check out the converted version of the application. To ensure that you have all the dependencies installed, also run pip install -r requirements.txt.

To run the unit tests, a custom command can be added to the flasky.py script. Example 7-10 shows how to add a test command.

The app.cli.command decorator makes it simple to implement custom commands. The name of the decorated function is used as the command name, and the function’s docstring is displayed in the help messages. The implementation of the test() function invokes the test runner from the unittest package.

The unit tests can be executed as follows:

Database Setup

The restructured application uses a different database than the single-script version.

The database URL is taken from an environment variable as a first choice, with a default SQLite database as an alternative. The environment variables and SQLite database filenames are different for each of the three configurations. For example, in the development configuration the URL is obtained from the environment variable DEV_DATABASE_URL, and if that is not defined then an SQLite database with the name data-dev.sqlite is used.

Regardless of the source of the database URL, the database tables must be created for the new database. When working with Flask-Migrate to keep track of migrations, database tables can be created or upgraded to the latest revision with a single command:

Running the Application

The refactoring is now complete, and the application can be started. Make sure you have updated the FLASK_APP environment variable as indicated in “Application Script”, and then run the application as usual:

Having to set the FLASK_APP and FLASK_DEBUG environment variables every time a new command-prompt session is started can get tedious, so you should configure your system so that these variables are set by default. If you are using bash, you can add them to your ~/.bashrc file.

Believe it or not, you have reached the end of Part I. You have now learned about the basic elements necessary to build a web application with Flask, but you probably feel unsure about how all these pieces fit together to form a real application. The goal of Part II is to help with that by walking you through the development of a complete application.

Powered by Forestry.md