0
votes

My flask app is based on Miguel Grinberg's mega tutorial XIV with some extra stuff and it was all working fine and could be accessed from browser on localhost:5000. I decided to switch to an application factory approach as per Grinberg's XV tutorial BUT with no blueprints. Now when I enter localhost:5000 I get a URL not found. I am guessing my routes.py is not being picked up for some reason.

I have thoroughly worked through Grinberg's XV tutorial and the associated code and aligned everything less blueprints. These are the links I have explored that I have based my current app on: -

https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure

http://flask.pocoo.org/docs/1.0/tutorial/factory/

Dave W Smith's answer in Configure Python Flask App to use "create_app" factory and use database in model class

and others.

From what I have read implementing an application factory should be really simple. The examples work.

The flask server starts from the command prompt in a venv as always.

Here is my directory structure.

myapp
    app.py
    config.py
    ...
    /app
        __init__.py
        routes.py
        models.py
        forms.py
        ...

Here is the code slightly simplified for clarity.

# app.py
#=============================================
from app import create_app, db
from app.models import User, Post

app = create_app()

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post' :Post}

# __init__.py
#=============================================
...
import os
from flask import Flask, request, current_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
...
from config import Config

db = SQLAlchemy()
migrate = Migrate()
...

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    ...

    # logging, email servers, etc configured

    return app

from app import models

# routes.py
#=============================================
from datetime import datetime
from flask import render_template, flash, redirect, url_for, request, g, \
    jsonify, current_app
...
from app import app, db
from app.forms import LoginForm, RegistrationForm, EditProfileForm, 
     PostForm,ResetPassword,RequestForm, ResetPasswordForm
from app.models import User, Post
...

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        ...
    page = request.args.get('page', 1, type=int)
    ...
    return render_template('index.html', title=_('Home'), form=form,
                           posts=posts.items, next_url=next_url,
                           prev_url=prev_url)

@app.route ....


# models.py
#=============================================
from datetime import datetime
from hashlib import md5
from time import time
from flask import current_app
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from app import db, login

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    ...

This may be irrelevant but it is one thing I tried. Note in routes.py that I have from app import app, db, if I remove app and add from flask import current_app then the @app decorators show as undefined.

Another point, because its a problem with routes. If I change the last line of __init__.py to from app import routes, models I get a different error.

flask.cli.NoAppException
flask.cli.NoAppException: While importing "metapplica", an ImportError was raised:

Traceback (most recent call last):
  File "c:\users\markko~1\dropbox\python\projects\metapp~1\venv\lib\site-packages\flask\cli.py", line 236, in locate_app
    __import__(module_name)
  File "C:\Users\Mark Kortink\Dropbox\Python\projects\metapplica\metapplica.py", line 1, in <module>
    from app import create_app, db, cli
  File "C:\Users\Mark Kortink\Dropbox\Python\projects\metapplica\app\__init__.py", line 75, in <module>
    from app import routes, models
  File "C:\Users\Mark Kortink\Dropbox\Python\projects\metapplica\app\routes.py", line 8, in <module>
    from app import app, db
ImportError: cannot import name 'app' from 'app' (C:\Users\Mark Kortink\Dropbox\Python\projects\metapplica\app\__init__.py)

It was this error that led me to try the current_app change mentioned above.

I start the app like this.

cd C:\Users\...\myapp
venv\Scripts\activate 
set FLASK_APP=app.py
set FLASK_DEBUG=1
flask run

I know the problem is probably really basic but can anyone see why the above code would give a URL not found or not be able to find the routes?

=== NEXT PHASE ================ Following the recommendations from @silver below my new names are: -

myapp
    runapp.py
    ...
    /mapp
        __init__.py
        ...

I went through all my code and changed from app[.xxx] import yyy to from myapp[.xxx] import yyy. This flushed out a few new errors where I was referencing app which I fixed by substituting current_app. My new error is

RuntimeError
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context().  See the
documentation for more information.

Traceback (most recent call last)
File "C:\Users\Mark Kortink\Dropbox\Python\projects\myapp\runapp.py", line 1, in <module>
from mapp import create_app, db, cli
File "C:\Users\Mark Kortink\Dropbox\Python\projects\myapp\mapp\__init__.py", line 75, in <module>
from mapp import routes, models
File "C:\Users\Mark Kortink\Dropbox\Python\projects\myapp\mapp\routes.py", line 16, in <module>
@current_app.before_request
3
Having an app.py and a package named app in one folder won't work. - Klaus D.
@KlausD. The app worked fine before with app.py and \app but I renamed app.py to be sure and still get the same problem. I added some more details to the end of the question. - Mark Kortink
Now you have a circular import: __init__.py -> routes.py -> __init__.py. Cirluar import don't work as expected. Unluckily flask is designed in a way that requires taking this into account. - Klaus D.
I would definitely recommend changing your names to be less ambiguous -- something like app_package at the top level, app_name.py as the FLASK_APP entry point, and app_internals as the inner directory. - silver

3 Answers

1
votes

Without being able to run it myself it's a little hard to be sure. But I think this is the crux of the matter:

This may be irrelevant but it is one thing I tried. Note in routes.py that I have from app import app, db, if I remove app and add from flask import current_app then the @app decorators show as undefined.

If you want to register anything to the app that's actually running, it has to be through from flask import current_app. That's the thing about Flask application factories -- you only get access to the name of the application that's actually running in two places:

  1. In the factory function itself: create_app, before you return the app object, and
  2. Via from flask import current_app

It looks like Python is able to successfully import the name app in routes.py, since you say the application starts. The only place that from app import app could resolve to is from your app.py file at the top level of your myapp package. That means that each time routes.py is initialized, it's calling the create_app function, and getting a new app object. So understandably, the top-level app object that Flask is serving is not the same one that has routes registered to it. I recommend renaming your files so that nothing has the name "app" except the object returned by create_app.

Then, in routes.py try:

from datetime import datetime
from flask import render_template, flash, redirect, url_for, request, g, \
    jsonify
from flask import current_app as app_ref
...
from app_internals import db  # assuming you've renamed the app package
from app_internals.forms import LoginForm, RegistrationForm, EditProfileForm, 
     PostForm,ResetPassword,RequestForm, ResetPasswordForm
from app_internals.models import User, Post
...

current_app =  app_ref._get_current_object()

@current_app.route('/', methods=['GET', 'POST'])
@current_app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        ...
    page = request.args.get('page', 1, type=int)
    ...
    return render_template('index.html', title=_('Home'), form=form,
                           posts=posts.items, next_url=next_url,
                           prev_url=prev_url)

@current_app.route ....
1
votes

In case someone is struggling with similar issues (flask 'mysteriously' not serving routes that should be there, perhaps in conjunction with a create_app factory method):

  1. flask routes is actually showing the routes it detects. the output looks like this (for just one 'hello' endpoint at the root url):

    Endpoint  Methods  Rule
    --------  -------  -----------------------
    hello     GET      /
    static    GET      /static/<path:filename>
    
  2. the SERVER_NAME setting can be responsible (this was the case for me). I had changed it to 0.0.0.0 without realizing it makes a difference for the routing. There is a bug report/discussion on github on that topic.

0
votes

I just ran into this issue too with Miguel Grinberg's tutorial. It's a pretty fantastic tutorial till some of the later sections which feel rushed.

Anyway. You need to pass the context. In the factory function you need to do.

    def create_app(config_class=Config):
        app = Flask(__name__)
        with app.app_context():
            from app import routes

Then like the person posted above. for routes.py change all the @app to @current_app

    from flask import current_app
    @current_app.route('/')
    @current_app.route('/index', methods=['GET', 'POST'])
    @login_required
    def index():

this works for me.