1
votes

TL;DR: How do I use a metadata object from a Blueprint to create the Flask-SQLAlchemy instance? The only place I can see to provide the declarative base metadata object is in the initial SQLAlchemy() call. But when I import it from the Blueprint in my extensions.py file, the Blueprint's code needs the db object and the loading fails due to a circular import.


I have several model classes which I would like to use both in and outside of Flask. I am using the declarative method to do this, and my app is set up to use the App Factory model and Blueprints. The way the models get registered with SQLAlchemy is through the use of the metadata argument when the db object is created. In the context of my application, it makes sense to declare the metadata object in a Blueprint, instead of in the main app Blueprint. (This is where most of the code which references it will be, including the non-Flask utility script used for initially populating the database.) However, importing the model class from the second Blueprint ends up in a circular import.

$ flask db migrate
Error: While importing "my_app", an ImportError was raised:

Traceback (most recent call last):
  File "my_app/venv/lib/python3.7/site-packages/flask/cli.py", line 235, in locate_app
    __import__(module_name)
  File "my_app/my_app.py", line 1, in <module>
    from app import create_app
  File "my_app/app/__init__.py", line 7, in <module>
    from app.extensions import *
  File "my_app/app/extensions.py", line 10, in <module>
    from turf.models import metadata
  File "my_app/turf/__init__.py", line 1, in <module>
    from .routes import bp
  File "my_app/turf/routes.py", line 14, in <module>
    from app.extensions import db
ImportError: cannot import name 'db' from 'app.extensions' (my_app/app/extensions.py)

As mentioned in this general question on circular imports for Blueprints, a solution which works is to import the db object from inside each function in the second Blueprint, thus side-stepping the import during initialization of the extensions.py file. But in addition to being annoying, this just feels extremely hacky.

Ideally, I would be able to pass the metadata object I've created to SQLAlchemy's init_app() method. That would resolve this issue at a stroke. Unfortunately, init_app() doesn't take a metadata argument. Is there some other way to register metadata with an SQLAlchemy instance after initialization? Or have I missed some other key element of the declarative model approach?

I should say that the non-Flask portion of this is working just fine. My utility script is able to import the models and use them to add objects to the database. It's only the Flask imports which are giving me trouble.

Here is the hierarchy:

.
├── app
│   ├── __init__.py
│   └── extensions.py
└── turf
    ├── __init__.py
    ├── models.py
    └── routes.py

And the relevant code which fails due to circular import:

app/__init__.py:

from app.extensions import *

def create_app():
    app = Flask(__name__)

    with app.app_context():
        import turf
        app.register_blueprint(turf.bp)

        db.init_app(app)

app/extensions.py:

from turf.models import metadata

db = SQLAlchemy(metadata=metadata)

turf/__init__.py:

from .routes import bp

turf/models.py:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import MetaData

metadata = MetaData()
Base = declarative_base(metadata=metadata)

# All the turf models are declared in this file
class Boundary(Base):
    # ...etc...

turf/routes.py:

from .models import *
from app.extensions import db

bp = Blueprint('Turf', __name__, url_prefix='/turf')

@bp.route('/')
def index():
    return render_template('turf/index.html')
1
please summarize your writeup. this is more like writing a textookNancy Moore
I've added a summary, thanks. Part of the problem is resolved if I register the blueprint AFTER initializing the extensions. Flask will run then, but flask_migrate (Alembic) still complains.Nick K9
(This turns out not to be true. flask run is still failing with the same circular import error. No idea why it worked on reload for a bit and then stopped.)Nick K9

1 Answers

1
votes

It turns out you can declare the MetaData object in the extensions.py file and then import that in the Blueprint. I was sure this would fail, because the metadata object is now being populated after the db object is created, but I've verified that the models are indeed available and work as expected. And no more circular dependency. I've actually broken this part out into its own file to allow Blueprint code to import as little as possible.

app/base.py:

from sqlalchemy import MetaData
from sqlalchemy.ext.declarative import declarative_base

metadata = MetaData()
Base = declarative_base(metadata=metadata)

app/extensions.py:

from flask_sqlalchemy import SQLAlchemy
from .base import metadata

db = SQLAlchemy(metadata=metadata)

turf/models.py:

from app.base import Base

# All the turf models are declared in this file
class Boundary(Base):
    # ...etc...

This also answers another question I had with my original approach: how would it work if I had a second Blueprint which also had model objects which needed to be available from non-Flask code? Now, I have created a single Base object and can use that to implement new classes in different Blueprints as needed.

One minor annoyance with this approach, though. In the non-Flask DB population script, I was originally able to use from models import * to refer to the sibling module (file) containing the models. This let me call the script directly, à la cd turf; python populate_db.py --arg. That no longer works because the models.py file now references a different package, app.extensions. So instead I have to use this workaround:

turf/populate_db.py:

try:
    from .models import *
except:
    print("You have to run this from outside the 'turf' directory like so: $ python -m turf.populate_db [...]")
    sys.exit(1)