Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No Context shown in SQLAlchemy tab #96

Open
rhaamo opened this issue Jun 15, 2016 · 3 comments
Open

No Context shown in SQLAlchemy tab #96

rhaamo opened this issue Jun 15, 2016 · 3 comments

Comments

@rhaamo
Copy link

rhaamo commented Jun 15, 2016

The context column only shown <unknown> on all of my pages.
I'm using Blueprints, can this cause issues ?
Versions used (not only but related to debugtoolbar):

Flask==0.10.1
Flask-SQLAlchemy==2.1
Flask-DebugToolbar==0.10.0
SQLAlchemy==1.0.12

Config extract:

DEBUG = True
TESTING = True
SQLALCHEMY_ECHO = True
SQLALCHEMY_RECORD_QUERIES = True

Edit: making queries from the "main" app file shows context, anything outside seems <unknown>

@sarimak
Copy link

sarimak commented Sep 6, 2016

I am using blueprints too and I had the same issue, the context is "unknown". I am able to get non-empty Context if my WSGI app is wrapped by ReverseProxied middleware (http://flask.pocoo.org/snippets/35/) -- but the Context is then always pointing to the middleware and not the actual code.

Flask (0.11.1)
Flask-DebugToolbar (0.10.0)
Flask-SQLAlchemy (2.1)
SQLAlchemy (1.0.12)

See below an app for reproducing the issue. There are several "tunables" which all are True except "classic" in my production system: reverse_proxied, classic, profiled, blueprinted. Only reverse_proxied seems to have impact on the Context in the debug toolbar.

If I would put all modules into one (mock.py), the Context column would display the correct function and line. So the culprit is perhaps hidden in the module imports -- but I am not able to find it.

mock.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from mock_app import app, db, classic
import mock_view
from mock_model import BaseModel, User

if __name__ == "__main__":
    if classic:
        session = db.session
        meta = db
        args = []
    else:
        engine = create_engine("sqlite:///mock.db")
        session = sessionmaker(bind=engine)()
        meta = BaseModel.metadata
        args = [engine]

    meta.drop_all(*args)
    meta.create_all(*args)
    session.add(User(id=1, name="John"))
    session.commit()

    app.run()

mock_model.py:

from sqlalchemy.ext.declarative import declarative_base

from mock_app import db, classic

if classic:
    BaseModel = db.Model
else:
    BaseModel = declarative_base()

if classic:
    Integer = db.Integer
    Column = db.Column
    String = db.String
else:
    from sqlalchemy import Integer, Column, String

class User(BaseModel):
    __tablename__ = "users"  # Not needed by classic

    id = Column(Integer, primary_key=True)
    name = Column(String)

mock_app.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from werkzeug.contrib.profiler import ProfilerMiddleware

reverse_proxied = True  # Context is empty if False and ./mock_app.py:27 (__call__) if True
classic = True  # The Flask-SQLAlchemy way
profiled = False

class ReverseProxied:
    def __init__(self, wsgi_app):
        self.app = wsgi_app

    def __call__(self, environ, start_response):
        script_name = environ.get("HTTP_X_SCRIPT_NAME", "")
        scheme = environ.get("HTTP_X_SCHEME", "")

        if script_name:
            environ["SCRIPT_NAME"] = script_name
            path_info = environ["PATH_INFO"]

            if path_info.startswith(script_name):
                environ["PATH_INFO"] = path_info[len(script_name):]

        if scheme:
            environ["wsgi.url_scheme"] = scheme

        return self.app(environ, start_response)

app = Flask(__name__)
if reverse_proxied:
    app.wsgi_app = ReverseProxied(app.wsgi_app)

if profiled:
    app.config["PROFILE"] = True
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[5])

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///mock.db"
if classic:
    db = SQLAlchemy(app)
else:
    db = SQLAlchemy(session_options={"autocommit": True})
    db.init_app(app)

from flask_sqlalchemy import _EngineDebuggingSignalEvents  # noqa
_EngineDebuggingSignalEvents(db.get_engine(app), app.import_name).register()  # Display SQL queries issued by SQLAlchemy

from flask_debugtoolbar import DebugToolbarExtension

app.debug = True
app.config["SECRET_KEY"] = "123"
app.config["DEBUG_TB_PROFILER_ENABLED"] = True
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
debug_toolbar = DebugToolbarExtension(app)
app.extensions["debugtoolbar"] = debug_toolbar

mock_view.py:

from flask import Blueprint
from mock_app import app, db
from mock_model import User

blueprinted = False

if blueprinted:
    blueprint = Blueprint("blueprint", __name__, template_folder="templates", url_prefix="")
else:
    blueprint = app

@blueprint.route("/")
def hello():
    username = db.session.query(User.name).filter_by(id=1).scalar()
    return "<html><head/><body>Hello {}!</body></html>".format(username)

if blueprinted:
    app.register_blueprint(blueprint)

@blurrcat
Copy link

Here's how Flask-SQLAlchemy generates the context(_calling_context):
From the most inner frame which issues a db query, it goes upwards to find the frame that matches the "app_path", and set that as the context.

app_path is what you pass to Flask when you create the application. Often times you'd use something like app = Flask(__name__) in your app.py. And this is where things go south: your app_path is now something like project.app, but the frame that issues a db query is probably project.views. _calling_context matches the names by checking name.startwith(app_path + '.'). project.views never passes that check.

To fix this, simply explicitly pass the name of your app to Flask, like app = Flask('project'). Now app_path is project and it can correctly match frames in your code.

@sarimak
Copy link

sarimak commented Apr 19, 2017

Wow, thank you!

My Flask application resides in a module app.py in a package called the same as my application. The SQLAlchemy queries are located in several modules (using the "gateway" pattern - DB calls/persistence layer are decoupled both from the presentation/view and the business logic) located in sub-packages (=blueprints ="micro-services") of the main application package.

Changing the name of the Flask app from __name__ to "name of application package" did the trick. So hard-coding the top-level package name works regardless of the inner structure of the Flask application and regardless of the WSGI middleware and blueprints.

Could you please update the documentation (especially https://flask-debugtoolbar.readthedocs.io/en/latest/#usage ) with the hard-coded value + could you please add a warning that such approach may be necessary and why? (or even better: could you change the discovery code to be compatible with the default __name__ approach? the __name__ is used even on Flask's homepage, see http://flask.pocoo.org/)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants