I am using Flask Blueprints to create an application. I am testing it with pytest. Generating test_client in different states with pytest.fixture is causing a blueprint name collision.
This is my Flask __init__ code:
from flask import Flask
from flask_dropzone import Dropzone
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_user import UserManager
from flask_googlemaps import GoogleMaps
import os
from app.settings import app, db
# get extensions
dropzone = Dropzone()
google_maps = GoogleMaps()
migrate = Migrate()
def create_app(config_class=Config):
# initialise extensions
google_maps.init_app(app)
dropzone.init_app(app)
db.init_app(app)
migrate.init_app(app, db)
# FLASK-User Stuff
from app.models import User
user_manager = UserManager(app, db, User)
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
I am then trying to use multiple pytests to test different aspects of the application. For each I am creating a pytest.fixture to generate a test_client. For example I'm creating two fixtures for testing the app with the db in different states.
For example.
#pytest.fixture(scope='module')
def client_state1():
app = create_app()
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + PATH_TO_DB_STATE_1
app.config['TESTING'] = True
client = app.test_client()
ctx = app.app_context()
ctx.push()
yield client
ctx.pop()
#pytest.fixture(scope='module')
def client_state2():
app = create_app()
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + PATH_TO_DB_STATE_2
app.config['TESTING'] = True
client = app.test_client()
ctx = app.app_context()
ctx.push()
yield client
ctx.pop()
def test_with_state1(client_state1):
"""Test should pass"""
assert 1 == 1
def test_with_state2(client_state2):
"""Test should pass"""
assert 1 == 1
When I run the tests I get the following error.
AssertionError: A blueprint's name collision occurred between <flask.blueprints.Blueprint object at 0x1a16b5d4e0> and <flask.blueprints.Blueprint object at 0x1a1633f6a0>. Both share the same name "googlemaps". Blueprints that are created on the fly need unique names.
I have tried splitting out tests into separate files and setting the scope of the fixture to function. But to no avail. In all cases the first test passes and the second causes the collision. What am I doing wrong?
Related
I am building a flask application and I have used a 'create_app' factory function to create an instance of the application. The factory function is as follows:
from config import Config
from flask import Flask
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate(render_as_batch=True)
def create_app(config_class: Config):
load_dotenv()
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp)
return app
I am using this pattern because I intend to use different configurations across different instances of my application i.e development, testing and production instances. This functionality is implemented by creating a configuration class within a config.py file, storing the configurations as attributes within the configuration class and passing this class as an argument to the create_app() function. For example, an app to be used during development can be created as follows:
from app import create_app
from config import DevelopmentConfig
app = create_app(config_class=DevelopmentConfig)
app.run(port=5500)
The following config classes exist within config.py:
import os
class Config(object):
SQLALCHEMY_TRACK_MODIFICATIONS = False
TESTING = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
class TestingConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get("TEST_DATABASE_URL")
TESTING = True
While building my app's test_suite, I have created a different app instance for testing as follows:
from app import create_app
from config import TestingConfig
import pytest
#pytest.fixture(scope="session")
def app():
app = create_app(config_class=TestingConfig)
return app
I have created a different testing instance because I intend to separate resources I use for testing from those I use for development, most notably the database.
To confirm that the resources are indeed separated, I have written a test to confirm my testing instance's configurations:
import os
def test_app_configurations(app):
assert app.config["TESTING"] is True
assert app.config["SQLALCHEMY_DATABASE_URI"] == os.environ.get("TEST_DATABASE_URL")
This test however fails and I am hit with the following assertion error:
def test_app_configurations(app):
assert app.config["TESTING"] is True
> assert app.config["SQLALCHEMY_DATABASE_URI"] == os.environ.get("TEST_DATABASE_URL")
E AssertionError: assert None == 'postgresql://test_user:password#localhost:5432/test_db'
To make this test pass, I have to explicitly declare my database URL in my app fixture after the create_app() function as follows:
#pytest.fixture(scope="session")
def app():
app = create_app(config_class=TestingConfig)
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("TEST_DATABASE_URL")
return app
I am however a bit confused. When running a development instance of the application, the attributes declared in the DevelopmentConfig class are automatically attached to the development instance. Why is that not the case for the testing instance. Why do I have to explicitly re-declare what I have already declared in the TestConfig class attributes? Is there something else I can do to make the test pass without having to reassign instances during testing when I already assigned the as class attributes in the TestConfig?
I was trying to access a sqlite db I use on development server, but I got following error:
'NoneType' object has no attribute 'drivername'. It happend both during calling business logic from a view or during calling db.create_all() inside test client definition (currently removed from code).
Test file code:
import os
import json
import pytest
from flask import Flask
import run
from src.utils.config import TestingConfig
from src.models.user_model import User
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
#pytest.fixture
def client():
app = run.create_app(TestingConfig)
context = app.app_context()
context.push()
with context:
with app.test_client() as client:
yield client
context.pop()
def test_user_register(client):
user_data = {
'login': 'login',
'password': 'password',
'email': 'email#example.com',
'first_name': 'first_name',
'last_name': 'last_name',
'index': '000000'
}
rv = client.post('/api/v1/users/register', data=json.dumps(user_data), content_type='application/json')
user_data['user_role'] = 2
assert '' == rv.data
Create app code:
from flask import Flask, current_app
from flask_cors import CORS
from flask_swagger_ui import get_swaggerui_blueprint
from src.utils.config import Config, DevelopmentConfig, ProductionConfig
from src.utils.extensions import db, ma, migrate, cors
from src.views import user_view, task_view, task_submission_view
from src.exceptions import exceptions_view
def create_app(config):
app = Flask(__name__)
app.config.from_object(config)
app.register_blueprint(exceptions_view.handler)
app.register_blueprint(user_view.blueprint, url_prefix='/api/v1/users')
app.register_blueprint(task_view.blueprint, url_prefix='/api/v1/tasks')
app.register_blueprint(task_submission_view.blueprint, url_prefix='/api/v1/taskSubmissions')
swagger_blueprint = get_swaggerui_blueprint('/swagger', '/static/swagger.json')
app.register_blueprint(swagger_blueprint)
db.init_app(app)
ma.init_app(app)
migrate.init_app(app, db)
cors.init_app(app)
return app
.env fragment :
SQLALCHEMY_DATABASE_URI = "sqlite:///sqlite.db"
Answer:
When I created the app from an external script run.create_app() even though blueprints were registered correctly the configuration was wiped out after the script execution.
After some trial-and-error runs I found out that configurating app by function app.config.from_object(), more precisely during os.getenv(), was an issue - even after moving it to the test file it wasn't able to configurate the app.
Solution was to set the configuration manually (inside or outside the test file):
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sqlite.db'
So i've been building a flask app just using an app.py file and running it.
It has quite a big app now and i'm now just trying to convert it into an application factory because I need to use SQLAlchemy in my Celery tasks.
here is my init.py in my app folder
def create_app():
load_dotenv(".env")
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["BROKER_URL"] = os.getenv("REDIS_BACKEND_BROKER")
app.config["CELERY_BROKER_URL"] = os.getenv("REDIS_BACKEND_BROKER")
app.config["CELERY_IMPORTS"] = "celery_tasks"
app.secret_key = os.getenv("SECRET_KEY")
CORS(app)
api = Api(app)
jwt = JWTManager(app)
db.init_app(app)
ma.init_app(app)
celery.init_app(app)
#app.before_first_request
def create_tables():
db.create_all()
#jwt.invalid_token_loader
def invalid_token_callback(self):
return {"message": "invalid"}, 401
with app.app_context():
from .resources.auth import Auth, CheckUser
from .resources.period import Period
from .resources.project import Project
from .resources.session import Session
api.add_resource(Auth, "/auth")
api.add_resource(CheckUser, "/check")
api.add_resource(Project, "/createproject")
api.add_resource(Period, "/createperiod")
api.add_resource(Session, "/createsession")
return app
The problem is that all the resources that being imported breaks because they can no longer import based on modules either.
For example resources.period also imports SQLAlchemy models and Masrhmallow schemas
resources/period.py
#THESE ARE NO LONGER IMPORTED SUCCESSFULLY
from models.project import ProjectModel
from schemas.task import TaskSchema
from schemas.period import PeriodSchema
Here is my file structure
This is an awesome tutorial by Miguel Grinberg where he refactores a complete application like you want it, too:
https://www.youtube.com/watch?v=NH-8oLHUyDc&t=2934s
Did you try to make an "absolute" import like:
from app.models.project import ProjectModel
Since you're importing from resources/period.py using relative imports, you need to go up a level:
from ..models.project import ProjectModel
from ..schemas.task import TaskSchema
from ..schemas.period import PeriodSchema
I'm have flask init with create app function. I create test file for my unitests. In unitest class add setUp function were i create new flask app and add context, push context to it. Next i create test db with create_all() function and, where i start test file, i have next error:
in _execute_for_all_tables op(bind=self.get_engine(app, bind), **extra)
TypeError: create_all() got an unexpected keyword argument 'forms'
I haven't 'forms' files or variable on models or anywere.
Before this error place in flask_sqlalchemy/init.py MetaData(bind=None), may be it some help.
from flask import Flask
from config import Config
from blocker_application.database import db
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_moment import Moment
migrate = Migrate()
login = LoginManager()
login.login_view = 'user.login'
moment = Moment()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
moment.init_app(app)
from blocker_application.main.routes import bp_main
from blocker_application.errors.handlers import bp_error
from blocker_application.reports.routes import bp_reports
from blocker_application.user.routes import bp_user
from blocker_application.applications.routes import bp_applications
app.register_blueprint(bp_main)
app.register_blueprint(bp_error)
app.register_blueprint(bp_reports, url_prefix='/reports')
app.register_blueprint(bp_user, url_prefix='/user')
app.register_blueprint(bp_applications, url_prefix='/applications')
return app
from blocker_application import models
________________________________________________________________________
/config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'not realy finish secret key configuration'
SQLALCHEMY_DATABASE_URI = 'mysql://some_database'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class TestConfig(Config):
TESTING = True
SQLALCHEMY_BINDS = {'test': 'mysql://some_database_test'}
______________________________________________________________________
/tests.py
import unittest
from blocker_application import create_app, db
from blocker_application import models
from config import TestConfig
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app = create_app(TestConfig)
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all(bind='test')
def tearDown(self):
db.session.remove()
db.drop_all(bind='test')
self.app_context.pop()
def test_password(self):
u = models.User(username='Mark')
u.set_password('Mark')
self.assertTrue(u.check_password('Mark'))
self.assertFalse(u.check_password('Tony'))
if __name__ == '__main__':
unittest.main(verbosity=2)
I found decision. Unittest work ok, after recreate the virtual enveroment.
I want to structure my Flask app something like:
./site.py
./apps/members/__init__.py
./apps/members/models.py
apps.members is a Flask Blueprint.
Now, in order to create the model classes I need to have a hold of the app, something like:
# apps.members.models
from flask import current_app
from flaskext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(current_app)
class Member(db.Model):
# fields here
pass
But if I try and import that model into my Blueprint app, I get the dreaded RuntimeError: working outside of request context. How can I get a hold of my app correctly here? Relative imports might work but they're pretty ugly and have their own context issues, e.g:
from ...site import app
# ValueError: Attempted relative import beyond toplevel package
The flask_sqlalchemy module does not have to be initialized with the app right away - you can do this instead:
# apps.members.models
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Member(db.Model):
# fields here
pass
And then in your application setup you can call init_app:
# apps.application.py
from flask import Flask
from apps.members.models import db
app = Flask(__name__)
# later on
db.init_app(app)
This way you can avoid cyclical imports.
This pattern does not necessitate the you place all of your models in one file. Simply import the db variable into each of your model modules.
Example
# apps.shared.models
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# apps.members.models
from apps.shared.models import db
class Member(db.Model):
# TODO: Implement this.
pass
# apps.reporting.members
from flask import render_template
from apps.members.models import Member
def report_on_members():
# TODO: Actually use arguments
members = Member.filter(1==1).all()
return render_template("report.html", members=members)
# apps.reporting.routes
from flask import Blueprint
from apps.reporting.members import report_on_members
reporting = Blueprint("reporting", __name__)
reporting.route("/member-report", methods=["GET","POST"])(report_on_members)
# apps.application
from flask import Flask
from apps.shared import db
from apps.reporting.routes import reporting
app = Flask(__name__)
db.init_app(app)
app.register_blueprint(reporting)
Note: this is a sketch of some of the power this gives you - there is obviously quite a bit more that you can do to make development even easier (using a create_app pattern, auto-registering blueprints in certain folders, etc.)
an original app.py: https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/
...
app = flask.Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = flask.ext.sqlalchemy.SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
...
class Computer(db.Model):
id = db.Column(db.Integer, primary_key=True)
...
# Create the database tables.
db.create_all()
...
# start the flask loop
app.run()
I just splitted one app.py to app.py and model.py without using Blueprint. In that case, the above answer dosen't work. A line code is needed to work.
before:
db.init_app(app)
after:
db.app = app
db.init_app(app)
And, the following link is very useful.
http://piotr.banaszkiewicz.org/blog/2012/06/29/flask-sqlalchemy-init_app/