Flask is one of the most popular Python frameworks, but some mistakes that occur when using it may lead to certain difficulties. In this article, we will present the topic on how to prevent cyclic imports in a project.
Flask and Cyclic Imports
Developers often face the problem of dependencies between modules while using Flask. To create view and models, a developer uses global objects created and initialized in the main module (in "front controller"). At the same time, there is a risk that cyclic imports will occur and it will be difficult to maintain a project.
Flask documentation and basic tutorials suggest to write a project initialization code in __init__.py to solve the problem. This code creates Flask instance of a class and configures an app. It allows to get access to all global objects from a visibility area of a package.
When using this approach, the structure looks like:
.
├── app
│ ├── __init__.py
│ ├── forms.py
│ ├── models.py
│ ├── views.py
│ └── templates
├── config.py
└── migrations
app/__init__.py
import flask
from flask_mail import Mail
app = Flask(__name__)
mail = Mail(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 so good since all the components are strongly connected. Subsequently, it will be difficult to elaborate such project as changing the code in one place will lead to changes in a dozen of other places.
As a rule, we solve the problem as follows:
- We avoid standard routing.
- We prefer original versions of libraries, without "wrapping".
- Using dependency injection.
Let's focus on this in more depth.
Working With Сlassy
Instead of using the standard routing method described in the documentation, you can use the classy approach. With this approach, you don't have to manually write routing for view: it will be configured automatically based on names of your classes and methods. This approach allows to improve the structure of a code, as well as to create view without app object. As a result, the problem with cyclic import is solved.
The example of the project structure while working with the flask-classful library:
.
├── 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
from app import routes as app_route
app = Flask(__name__)
mail = Mail(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)
app/views.py
from flask_classy import FlaskView, route
from flask_login import login_required
class AccountView(FlaskView):
def login(self):
pass
@login_required
def logout(self):
pass
When examining the code, you should pay attention to the fact that initialization now takes place in app.py, which is located at the root. An app is divided into sub projects that are configured by blueprint and then are registered in app object by only one line of code.
Original Libraries are More Preferable
The code presented above shows how flask-classful helps to cope with cyclic imports. This problem in classic Flask projects occurs due to both view declaration and some extensions. One of the best examples is flask-sqlalchemy
.
The flask-sqlalchemy
extension is designed to improve the integration between sqlalchemy
and flask
, but in practice it often brings more problems than benefits:
- The extension promotes the use of a global object for working with the database, including models’ creation, which again leads to the problem of cyclic imports.
- There is a need to describe models using your own classes, which lead to a tight binding of models to the Flask project. As a result, these models cannot be used in subprojects or in a supporting script.
We try not to use flask-sqlalchemy
for these reasons.
Using the Dependency Injection Pattern
The implementation of the classy approach and the rejection of flask-sqlalchemy
are just first steps to solve the problem of cyclic import. Next, you need to realise the logic to get an access to global objects in the application. For this purpose, it is good to use the dependency injection pattern implemented in the dependency-injector library.
The example of using a pattern in the code with the 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)
app.register_blueprint(app_routes.app_blueprint)
class DIServices(di_cnt.DeclarativeContainer):
mail = di_prv.Object(mail)
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='/')
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 mentioned in this article allow to get rid of cyclic imports, as well as to improve the quality of a code. We suggest to look at Flask project using approaches described above, through the example of the "Bulls and cows" game, designed in the form of the web app.
Conclusion
We've considered tips to overcome a common architectural problem of Flask apps related to cyclic imports. Using them, you can simplify maintenance and refactoring of applications.
Thank you for your attention! We hope that this article was useful for you.
History
- 23rd April, 2020: Initial version