While Flask is a lightweight framework, it also provides many convenient features such as unit testing and database migration for large Web applications, so let's look at the structure of building large Web applications using the Python flask Framework Example:
While small Web applications can be handy with a single script, this approach does not scale well. As applications become more complex, processing in a single large source file can become problematic.
Unlike most other web frameworks, flask does not have a specific organizational approach to large projects, and the structure of the application is entirely left to the developer to decide. In this chapter, a possible way to organize packages and modules for managing a large application is presented. This structure will be used in the rest of the book.
1. Project structure
Example basic multi-file flask application Structure
|-flasky |-app/ |-templates/ |-static/ |-main/ |-__init__.py |-errors.py |-forms.py |-views.py |-__init__.py |-email.py |-models.py |-migrations/|-tests/ |-__init__.py |-test*.py |-venv/|-requirements.txt |-config.py |-manage.py
This structure has four top-level catalogs:
Flask apps are typically placed under a directory called apps.
The Migrations directory contains the database migration script, as it was previously said.
Unit tests are placed in the test directory
The Venv directory contains the Python virtual environment, which is the same as it was previously said.
There are a few new files:
Requirements.txt lists some of the dependent packages so that you can easily deploy the same virtual environment on different computers.
The config.py stores some configuration settings.
manage.py is used to start applications and other application tasks.
To help you fully understand this structure, the following describes the process of changing the hello.py application to conform to this structure.
2. Configuration options
Applications typically require several configuration settings. The best example is the need to use different databases, tests, and production environments in the development process so that they do not interfere with each other.
We can use the configuration class hierarchy to replace the simple class dictionary structure configuration in hello.py. The config.py file is shown below.
config.py: Application Configuration
Import Osbasedir = Os.path.abspath (Os.path.dirname (__file__)) class Config:secret_key = Os.environ.get (' Secret_key ') Or ' hard to guess string ' Sqlalchemy_commit_on_teardown = True flasky_mail_subject_prefix = ' [Flasky] ' Flasky_mail_sen DER = ' Flasky Admin <flasky@example.com> ' flasky_admin = Os.environ.get (' flasky_admin ') @staticmethod def Init _app (APP): Passclass developmentconfig (Config): DEBUG = True mail_server = ' smtp.googlemail.com ' Mail_port = 587 Mail_use_tls = True Mail_username = os.environ.get (' mail_username ') Mail_password = Os.environ.get (' Mail_password ') SQ Lalchemy_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 \ ' sql ite:///' + os.path.join (basedir, ' Data-test.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 some of the same configurations, and different subclasses define different configurations. Additional configuration can be added when needed.
To make the configuration more flexible and secure, some settings can be imported from environment variables. For example, Secret_key, because of its sensitivity, can be set in the environment, but must provide a default value if there is no definition in the environment.
The Sqlalchemy_database_uri variable can be assigned a different value in three configurations. This allows the application to run under different configurations, each of which can use a different database.
A configuration class can define a Init_app () static method that takes an application instance as a parameter. Configuration-specific initialization is possible here. Here the config base class implements an empty Init_app () method.
At the bottom of the configuration script, these different configurations are registered in the configuration dictionary. Register one of the configurations (development configuration) as the default configuration.
3. Application Package
The application package places all the application code, templates, and static files. It is simply called an app, or it can be given an app-specific name, if needed. The templates and static directories are part of the app, so the two directories should be placed in the app. The database model and e-mail support functions are also placed into this package, each in the form of app/models.py and app/email.py in their own modules.
3.1. Use an application factory
Creating an application in a single file is convenient, but it has one big drawback. Because the application is created at the global scope, there is no way to dynamically adapt the changes to the application configuration: When the script runs, the application instance has already been created, so it is too late to change the configuration. This is especially important for unit testing because it is sometimes necessary to run the application under different configurations for better test coverage.
The solution to this problem is to put the application into a factory function to defer creation so that it can be explicitly called from the script.
This not only gives the script sufficient time to set up the configuration, but it can also be used to create multiple application instances-something that is very useful during testing. The application factory functions that are defined in the app package's constructor are shown in example 7-3.
This constructor imports most of the extensions that are currently needed, but because no application instance initializes them, it can be created but not initialized by the constructors that do not pass arguments to them. Create_app () is an application factory function that needs to pass in the configuration name for the application. The settings in the configuration are saved in a class in config.py and can be imported directly using the From_object () method of the Flask App. Config configuration object. The configuration object can be selected from the Config dictionary through the object name. Once the application is created and configured, the extension can be initialized. The initialization work is created and completed before calling Init_app () in the extension.
app/_init__.py: Application Package Constructor _
From flask import flask, render_template from flask.ext.bootstrap Import bootstrap from FLASK.EXT.MAIL import mailfrom fla Sk.ext.moment Import momentfrom flask.ext.sqlalchemy import sqlalchemy from config import configbootstrap = Bootstrap () ma Il = Mail () moment = Moment () db = SQLAlchemy () def create_app (config_name): app = Flask (__name__) App.config.from _object (Config[config_name]) Config[config_name].init_app (APP) Bootstrap.init_app (APP) Mail.init_ App Moment.init_app (APP) Db.init_app (APP) # Attach routes and custom error pages here return app
The factory function returns the instance of the application created, but note that applications created using the factory function in the current state are incomplete because they do not have a Routing and custom error page handler. This is the subject of the next section.
3.2. Implement the application function in blueprint
The transformation of the application factory leads to complications in routing. In a single-script application, the application instance is global, so it is easy to define the route using the App.route adorner. But now that the application is created at run time, the App.route adorner only begins to exist after the Create_app () call, which is too late. Like routing, these custom error page handlers defined by the App.errorhandler adorner also have the same problem.
Fortunately, Flask uses blueprints to provide a better solution. A blueprint is similar to an application that can define a route. The difference is that the blueprint associated with the route is dormant, and the route becomes part of it only when the blueprint is registered in the app. Using a blueprint defined under a global scope, defining an application's routing is almost as simple as a single-script application.
As with applications, blueprints can be defined in a file or in a package to create a more structured approach with multiple modules. For maximum flexibility, you can create sub-packages in the application package to hold blueprints. The constructor for creating a blueprint is shown below.
app/main/_init__.py: Creating Blueprints _
From flask Import Blueprintmain = Blueprint (' main ', __name__) from. Import views, errors
Blueprints are created by instantiating the Blueprint class object. The constructor for this class receives two parameters: the Blueprint name and the location of the module or package where the blueprint resides. As with an application, in most cases, the __name__ variable using Python for the second parameter value is correct.
The routing of the application is kept inside the app/main/views.py module, and the error handler is saved in app/main/errors.py. Importing these modules enables routing, error handling, and blueprints to be associated. It is important to note that importing modules at the bottom of the app/init.py script avoids circular dependencies because both view.py and errors.py need to import the main blueprint.
The blueprint is registered as an application in the Create_app () factory function, as shown below.
Example app/_init__.py: Blueprint Registration _
def create_app (config_name): # ... From. Main import main as Main_blueprint app.register_blueprint (main_blueprint) return app
Error handling is shown below.
app/main/errors.py: Error handling of blueprints
From flask import Render_template. Import Main@main.app_errorhandler (404) def page_not_found (e): return render_template (' 404.html '), 404@main.app_ ErrorHandler (+) def internal_server_error (e): return render_template (' 500.html '), 500
The difference in writing error handling in blueprints is that if you use the ErrorHandler adorner, only the error handling that is caused in the blueprint is invoked. For application-scoped error handling, you must use App_errorhandler.
This shows the application routing that was updated in the blueprint.
app/main/views.py: Application Routing with blueprints
From datetime import datetimefrom flask Import render_template, session, redirect, Url_forfrom. Import mainfrom. Forms Import Nameform from: Import Dbfrom. Models Import User@main.route ('/', methods=[' GET ', ' POST ']) def index (): form = Nameform () if Form.validate_on_ Submit (): # ... Return Redirect (Url_for ('. index ')) return render_template (' index.html ', form=form, Name=session.get (' Name '), known=session.get (' known ', False), Current_time=datetime.utcnow ())
There are two major differences in writing view functions in blueprints. First, as with previous error handling, the route adorner comes from the blueprint. The second difference is the use of the Url_for () function. You might recall that the first parameter of the function is the route node name, which specifies the default view function for the application-based route. For example, the URL of the index () view function in a single-script application can be obtained by url_for (' Index ').
The difference is that the flask namespace applies to all nodes from the blueprint, so that multiple blueprints can use the same node to define the view function without creating a conflict. The namespace is the blueprint name (the first parameter in the Blueprint constructor), so the index () view function is registered as Main.index and its URL can be obtained through url_for (' Main.index ').
In the blueprint, the Url_for () function also supports shorter-form nodes, omitting the blueprint name, such as Url_for ('. Index '). With this, you can use the blueprint for the current request. This actually means that redirects within the same blueprint can be used in a shorter form, and a node name with a namespace must be used if the redirect crosses the blueprint.
The application page changes are completed, and the form objects are saved in the blueprint in the app/main/forms.py module.
4. Startup script
The manage.py file in the top-level directory is used to launch the app.
manage.py: Startup script
#!/usr/bin/env pythonimport osfrom App import Create_app, Dbfrom app.models import User, rolefrom flask.ext.script Import Manager, Shellfrom flask.ext.migrate import migrate, Migratecommandapp = Create_app (os.getenv (' flask_config ') or ' Default ') Manager = Manager (APP) migrate = Migrate (app, DB) def make_shell_context (): return Dict (App=app, db=db, User =user, Role=role) manager.add_command ("Shell", Shell (Make_context=make_shell_context)) Manager.add_command (' db ', Migratecommand) If __name__ = = ' __main__ ': manager.run ()
This script starts with creating the application. Use the environment variable flask_config, if it is already defined, get the configuration from it, and if not, use the default configuration. The Flask-script, flask-migrate, and custom contexts used for the Python shell are then initialized.
For convenience, a one-line execution environment is added, which allows the./manage.py to execute Scripts on UNIX-based operating systems instead of lengthy Python manage.py.
5. Requirement Documents
The application must contain the Requirements.txt file to record all dependent packages, including the exact version number. This is important because virtual environments can be regenerated on different machines, such as deploying applications on machines in a production environment. This file can be generated automatically by the following PIP command:
(venv) $ pip Freeze >requirements.txt
It's a good idea to update this file again after installing or updating a package. An example of a requirement file is shown below:
flask==0.10.1flask-bootstrap==3.0.3.1flask-mail==0.9.0flask-migrate==1.1.0flask-moment==0.2.0flask-sqlalchemy= =1.0flask-script==0.6.6flask-wtf==0.9.4jinja2==2.7.1mako==0.9.1markupsafe==0.18sqlalchemy==0.8.4wtforms== 1.0.5werkzeug==0.9.4alembic==0.6.2blinker==1.3itsdangerous==0.23
When you need to replicate a virtual environment perfectly, you can run the following command to create a new virtual environment:
(venv) $ pip install-r requirements.txt
When you read this, the version number in the sample Requirements.txt file may be obsolete. If you like you can try to use the recently released package. If you encounter any problems, you can always fall back to the specified version that is compatible with your app in the requirements file.
6. Unit Test
The application is so small that it does not require much testing, but as an example, the sample shows two simple test definitions.
Example: tests/test_basics.py: Unit Test
Import unittestfrom flask Import Current_app from app import Create_app, Dbclass basicstestcase (unittest. TestCase): def setUp (self): Self.app = Create_app (' testing ') Self.app_context = Self.app.app_context () Self.app_context.push () Db.create_all () def tearDown (self): db.session.remove () db.drop_ All () Self.app_context.pop () def test_app_exists (self): self.assertfalse (Current_app is None) def test_app_is_testing (self): self.asserttrue (current_app.config[' testing ')
The well-written test uses the standard UnitTest package from the Python standard library. The SetUp () and teardown () methods run before and after each test, and any method must be executed with the start of test_ as a test.
Recommendation: If you want to learn more about using Python's unittest package to write unit tests, please refer to the official documentation.
The SetUp () method attempts to create a test environment similar to running the application. First it creates an application configuration for testing and activating the context. This step ensures that the test can access the same current_app as the regular request. Then, when needed, you can create a new database to use for testing. The database and application contexts are removed in the teardown () method.
The first Test ensures that an application instance exists. The second Test ensures that the application runs under a test configuration. To ensure that the tests directory is valid, you need to add the __init__.py file in the tests directory, but the file can be empty so that the UnitTest package can scan all modules and locate the test.
Recommendation: If you have cloned apps on GitHub, you can now run git checkout 7a to switch to this version of the app. To ensure that you have installed all of the dependent sets, you need to run Pip Install-r requirements.txt.
In order to run unit tests, you can add a custom command to the manage.py script.
The following shows how to add a Test command.
Example: MANAGE.PYT: Unit Test startup script
@manager. Commanddef Test (): "" " Run the unit tests. " "" Import unittest tests = unittest. Testloader (). Discover (' tests ') unittest. Texttestrunner (verbosity=2). Run (Tests)
The Manager.command decorator makes it easy to implement a custom command. The decorated function name can be used as the command name, and the function's document string displays help information. Execution of the test () function invokes the test runner in the UnitTest package.
Unit tests can be performed like this:
(venv) $ python manage.py test
Test_app_exists (test_basics. Basicstestcase) ... oktest_app_is_testing (test_basics. basicstestcase) ... ok.----------------------------------------------------------------------Ran 2 tests in 0.001sOK
7. Database Startup
A reconstructed application uses a different database than a single-script application.
The database URL obtained from the environment variable is preferred, and the default SQLite database is optional. The environment variables in three configurations are not the same as the SQLite database file names. For example, the URL for the development configuration is obtained from the DEV_DATABASE_URL environment variable, and a SQLite database named Data-dev.sqlite is used if it is not defined.
You must create a database table for the new database, regardless of the source of the database URL. If Flask-migrate is used to maintain migration tracking, the database table can be created or updated to the most recent version through the following command:
(venv) $ python manage.py db upgrade
Believe it or not, has reached the end of the first part of the place. You have now learned the essential elements of flask, but you are not sure how to combine these scattered knowledge to form a real application. The second part is designed to lead you through the development of a complete application.