Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does db.session.commit change app context in Flask-SQLAlchemy?

I am configuring a fixture for pytest that creates a flask app instance. My app is created with Application Factories pattern. I am at the stage of connecting it to the database and struggle to understand the difference between 2 patterns.

# project/__init__.py
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()


def create_app():
    app = Flask(__name__)

    app_settings = os.getenv('APP_SETTINGS')
    app.config.from_object(app_settings)

    db.init_app(app)

    [blueprint code]

    return app

In my fixture, I think I understand the need for:

  • db.create_all() during setup: create my tables
  • db.drop_all() during teardown: clean database after tests
  • db.session.remove() during teardown: avoid some weird locks on postgres when hitting the database frequently in tests

The first setup (inspired by Miguel Grinberg book) makes sense to me:

import pytest
from project import create_app, db


@pytest.fixture
def app():
    app = create_app()
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

It also matches the behavior I get in an interactive session, where I need to activate/push the app_context to bind the database:

Python 3.6.1 (default, Jun 21 2017, 18:45:41) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from project import create_app, db
>>> app = create_app()
>>> db
<SQLAlchemy engine=None>
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> db.create_all()
>>> db
<SQLAlchemy engine='postgres://postgres:postgres@users-db:5432/users_dev'>

The second setup (inspired by testdriven.io) also work in pytest but I don't know why:

import pytest
from project import create_app, db


@pytest.fixture
def app():
    app = create_app()
    db.create_all()
    db.session.commit()  # fail when this is removed
    yield app
    db.session.remove()
    db.drop_all()

Actually, if I try to do the same in an interactive session, I get an error:

Python 3.6.1 (default, Jun 21 2017, 18:45:41) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from project import create_app, db
>>> app = create_app()
>>> db.create_all()
[...] timeError: application not registered on db instance and no application bound to current context

I tried to run the fixture without db.session.commit(), thinking that maybe I was in the app context by default (similar to what I do with with app_context() in the first fixture). But it fails if I remove it.

like image 941
xav Avatar asked Jan 03 '23 18:01

xav


1 Answers

The first problem is that the second setup (by testdriven.io) is not using the Application Factory pattern, they're explicitly instantiating the db and binding the app (e.g., db = SQLAlchemy(app) versus db = SQLAlchemy() and a later db.init(app) in create_app()). If you were using the Application Factory pattern, you would get the error you're seeing in the interactive session after executing db.create_all(), keeping/removing db.session.commit() won't help either way.

I have an inkling that you're using two different from project import create_app, db for each attempt and using the Application Factory pattern for the interactive shell.

In any case, you're really asking two questions.

1) Why do I need to push an application context in order to run db.create_all() when using the Application Factory pattern?

If you look at the __init__ method in SQLAlchemy, you'll notice that you can pass through an app, in this case, that app becomes bound to the SQLAlchemy object, self.app = app. However, because you're using the Application Factory pattern, that app is never explicitly bound, even after you run db.init_app(app). Now look in create_all(), it takes an optional app, which you aren't passing through, so when we get to get_app, we skip over reference_app because it is None, we go over current_app, which looks at Flask's application context (see from flask import current_app), if you haven't pushed the context, this will also be None, and finally we check to see if there is a self.app, but this is also None because we are using the Application Factory pattern, hence the application not registered on db instance and no application bound to current context error.

2) Why do I need to explicitly run db.session.commit() after running db.create_all when binding the app during SQLAlchemy instantiation (e.g., db = SQLAlchemy(app))?**

I can't seem to reproduce this error, I've added a code block below to show you what I'm using, I got the pieces from the testdriven.io site. However, you shouldn't need to db.session.commit(), the only difference between using the Application Factory pattern and instantiating SQLAlchemy with the app (e.g., db = SQLAlchemy(app)) is that with the former you'll either need to pass in the app in create_all(app) or push an application context.

import pytest
import os
import datetime
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

db = SQLAlchemy(app)


@pytest.fixture
def app():
    app = create_app()
    db.create_all()
    # db.session.commit()  # Try this with and without this line
    yield app
    db.session.remove()
    db.drop_all()

References:

https://github.com/mitsuhiko/flask-sqlalchemy/blob/d71afea650e0186348d81f02cca5181ed7c466e9/flask_sqlalchemy/init.py

http://flask-sqlalchemy.pocoo.org/2.1/contexts/

like image 62
jackar Avatar answered Jan 06 '23 07:01

jackar