Flask web applications: how to deal with cyclic imports

Flask is one of the most popular Python frameworks, but some errors when using it can lead to certain difficulties. In this article we will talk about how to prevent the occurrence of cyclic imports in the project.

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *