2

I have a simple conceptual question about the Flask application factory pattern.

I'm trying to use Flask-Mail across different files. My __init__.py file is:

#
# __init__.py
#
from flask import Flask
from flask_pymongo import PyMongo
from flask_mail import Mail
from flask_login import LoginManager

mail = Mail()
mongo = PyMongo()
login_manager = LoginManager()

def create_app():
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object('config.DevConfig')

    mail.init_app(app)
    login_manager.init_app(app)
    mongo.init_app(app, retryWrites = False)

    with app.app_context():
        from .views import bp
        app.register_blueprint(views.bp)
    return app

And the other file:

#
# views.py
#
from flask import Blueprint
from flask import current_app as app
from flask_mail import Message

from app import mongo, mail, login_manager

bp = Blueprint('bp', __name__, template_folder='templates', static_folder='static')

@bp.route('/')
def index():
    msg = Message("Success", recipients=[ email ])
    with open('template.html', 'r') as fd:
        msg.html = fd.read()
    mail.send(msg)

Though I set MAIL_DEFAULT_SENDER in my config file, I'm getting the error that there is no default sender specified when hitting the mail.send(msg) line in views.py. After checking the mail object, I saw that it had no config variables set.

Per this tutorial, it seemed that I'd need to manually set current_app.config['MAIL_DEFAULT_SENDER'] whenever using the mail object under this pattern, and would need to write an additional with app.app_context(): block around the mail object such that it was instantiated with the proper config variables.

This seems like a lot of extra work, so is there another way to directly get the mail object that was initialized in create_app with all the proper config variables set?

Appreciate the help with this!

EDIT:

Created extensions.py:

from flask_pymongo import PyMongo
from flask_mail import Mail
from flask_login import LoginManager

mail = Mail()
mongo = PyMongo()
login_manager = LoginManager()

And modified __init__.py to be:

from flask import Flask

def create_app():
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object('config.DevConfig')

    from app.extensions import mail, login_manager, mongo
    mail.init_app(app)
    login_manager.init_app(app)
    mongo.init_app(app, retryWrites = False)

    with app.app_context():
        from .views import bp
        app.register_blueprint(views.bp)
    return app

And for views.py, I have:

from flask import Blueprint
from flask import current_app
from flask_mail import Message

from app.extensions import mongo, mail, login_manager

bp = Blueprint('bp', __name__, template_folder='templates', static_folder='static')

@bp.route('/')
def index():
    msg = Message("Success", recipients=[ email ])
    with open('template.html', 'r') as fd:
        msg.html = fd.read()
    mail.send(msg)
user2415992
  • 481
  • 7
  • 22

1 Answers1

4

It is an import problem. Please read the official documentation about Application Factories & Extensions.

Explanations:

  1. __init__.py is loaded, then mail object is created.

  2. Somewhere, create_app is called, then a new app object is created and uses the mail object previously created.

  3. While processing the create_app, blueprints are imported. (Why do you need load blueprints within the app_context ?)

  4. While blueprint is imported, it imports from app [...], mail. Imports are not shared among modules (in this case for sure). So __init__.py is executed again while being imported and created a new mail object that is not configured. (If you use a debugger you will see that there are 2 mail objects.)

In the end, you have 2 mail object: one is configured and one is not.

How to handle that:

As written in the documentation:

It’s preferable to create your extensions and app factories so that the extension object does not initially get bound to the application.

And they give the following example:

You should not do:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    db = SQLAlchemy(app)

But, rather, in model.py (or equivalent):

db = SQLAlchemy()

and in your application.py (or equivalent):

def create_app(config_filename):
   app = Flask(__name__)
   app.config.from_pyfile(config_filename)

   from yourapplication.model import db
   db.init_app(app)

Finally, you should create your mail object in a different file and import it your __init__.py. If you only use the mail object in this blueprint, it could be declared directly in this blueprint.

You can find a full example: Flaskr (Official example) that uses a DB extension as you use Flask-Mail.

Why is it so confusing? Most of the examples you can find on Google, do not use application factories and are always in one file. In their case, you will declare everything in the same file. In your case, you will share objects among modules, so they should be accessible correctly.

Victor
  • 602
  • 4
  • 12
  • Thanks so much for the reply! I moved everything to a separate file (`extensions.py`), as specified in the edit to the original question, but am still unable to get it to import `mail` correctly. Any thoughts on the above changes? – user2415992 Jun 07 '20 at 07:44
  • What is the error that you get ? Adding the stacktrace could be useful. – Victor Jun 07 '20 at 15:26
  • Unfortunately I don't get any error, the email just fails to send silently. I'm starting to think this is something Flask-Mail specific since Flask-Mongo and Flask-Login are working properly with this pattern. – user2415992 Jun 07 '20 at 20:30
  • I helped you with the mail object, but after that, without error logs, I can not do anything more. – Victor Jun 09 '20 at 19:11