65.9K
CodeProject is changing. Read more.
Home

Web-Apps on Flask: How to Deal With Cyclic Imports

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1 vote)

Apr 23, 2020

GPL3

3 min read

viewsIcon

15879

A bit about Python & Flask

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
# 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 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
# 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 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)

# blueprints registration
app.register_blueprint(app_routes.app_blueprint)

# providers creation
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='/')

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):
        # registration implementation
        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