Flask web applications: how to deal with cyclic imports
Flask and cyclic imports
Developers using flask are often faced with the problem of dependencies between modules. To declare a view and models, the developer uses global objects created and initialized in the main module (“entry point”). However, he risks receiving cyclical imports, which will make it difficult to maintain the project.
The documentation and basic flask tutorials for solving this problem suggest putting project initialization code in __init__.py, which creates the Flask instance classes and configures the application. This allows you to access all global objects from the scope of the package.
Using this approach, the structure looks something like this:
.
├── app
│ ├── __init__.py
│ ├── forms.py
│ ├── models.py
│ ├── views.py
│ └── templates
├── config.py
└── migrations
app / __ init__.py
import flask
from flask_mail import Mail
# other extensions
app = Flask(__name__)
mail = Mail(app)
# configure flask app
from app import views, models
app / views.py
from app import app
@app.route('/view_name/'):
def view_name():
pass
Obviously, this architecture is not very successful, since all components are strongly connected. Subsequently, it will be difficult to finalize such a project, because changing the code in one place will lead to changes in a dozen other places.
As a rule, we solve this problem as follows:
- Avoid standard routing.
- We prefer original versions of libraries, without “wrappers”.
- We use dependency injection.
Let’s take a closer look at this.
Work with classy
Instead of the standard routing method described in the documentation, you can use classy. With this approach, you do not need to manually write routing for view: it will be configured automatically based on the names of your classes and methods. This allows you to increase the structure of the code, as well as declare a view without an app object. As a result, it is possible to solve the problem of cyclical imports.
Example project structure when using a library flask-classful.
.
├── app
│ ├── static
│ ├── templates
│ ├── forms.py
│ ├── routes.py
│ ├── views.py
│ └── tasks.py
├── models
├── app.py
├── config.py
└── handlers.py
app.py
import flask
from flask_mail import Mail
# other extensions
from app import routes as app_route
app = Flask(__name__)
mail = Mail(app)
# configure flask app
app.register_blueprint(app_route.app_blueprint)
app / routes.py
from flask import Blueprint
from app import views
app_blueprint = Blueprint(...)
views.AccountView.register(app_blueprint)
# register other views
app / views.py
from flask_classy import FlaskView, route
from flask_login import login_required
class AccountView(FlaskView):
def login(self):
pass
# other views
@login_required
def logout(self):
pass
When studying the code, you need to pay attention to the fact that now the initialization takes place in the app.py located in the root. The application is divided into subprojects, which are configured using blueprint and are subsequently registered as a single line in the app object.
Preference for original versions of libraries
The above code shows how flask-classful helps deal with circular imports. The cause of this problem in classic flask projects can be both a view declaration and some extensions. One striking example is flask-sqlalchemy.
The flask-sqlalchemy extension is designed to improve the integration of sqlalchemy and flask, but in practice it often brings more problems than benefits to the project:
- The extension promotes the use of a global object for interacting with a database, including for declaring models, which again leads to the problem of cyclic imports.
- It is necessary to describe models using their own classes, which leads to tight binding of models to the flask project. As a result, these models cannot be used in subprojects or in an auxiliary script.
For these reasons, we try not to use flask-sqlalchemy.
Using the Dependency injection pattern
Implementing the classy approach and abandoning flask-sqlalchemy are just the first steps to solve the problem of circular import. Next, you need to implement the logic of access to global objects in the application. For this, it is convenient to use the dependency injection pattern implemented in the library dependency-injector.
An example of using a pattern in code with a dependency-injector library:
app.py
import dependency_injector.containers as di_cnt
import dependency_injector.providers as di_prv
from flask import Flask
from flask_mail import Mail
from app import views as app_views
from app import routes as app_routes
app = Flask(__name__)
mail = Mail(app)
# регистрация blueprints
app.register_blueprint(app_routes.app_blueprint)
# создание providers
class DIServices(di_cnt.DeclarativeContainer):
mail = di_prv.Object(mail)
# injection
app_views.DIServices.override(DIServices)
app / routes.py
from os.path import join
from flask import Blueprint
import config
from app import views
conf = config.get_config()
app_blueprint = Blueprint(
'app', __name__, template_folder=join(conf.BASE_DIR, 'app/templates'),
static_url_path='/static/app', static_folder='static'
)
views.AccountView.register(app_blueprint, route_base="https://habr.com/")
app / views.py
import dependency_injector.containers as di_cnt
import dependency_injector.providers as di_prv
from flask_classy import FlaskView
from flask_login import login_required
class DIServices(di_cnt.DeclarativeContainer):
mail = di_prv.Provider()
class AccountView(FlaskView):
def registration(self):
# реализация регистрации
msg = 'text'
DIServices.mail().send(msg)
def login(self):
pass
@login_required
def logout(self):
pass
The measures listed in the article can eliminate cyclic imports, as well as improve the quality of the code. We offer to see how a flask-project looks like using the approaches described above, using the example of the game “Bulls and Cows”, made in the form web applications.
Conclusion
We examined how to solve the common architectural problem of flask applications related to cyclic imports. With their help, you can simplify the development and support of your applications.
Thanks for attention! We hope you find this article helpful.