flask-sqlalchemy icon indicating copy to clipboard operation
flask-sqlalchemy copied to clipboard

object has no attribute 'drivername' if using only binds, not default DB

Open colegleason opened this issue 5 years ago • 3 comments

Expected Behavior

I have a flask project with two binds set in SQLALCHEMY_BINDS and with SQLALCHEMY_DATABASE_URI not specified. I would like this to work as long as __bind_key__ is properly specified for each model. I don't want to have a default DB, as I think that might lead to mistakes.

@pytest.fixture
def app():
    db_1_fd, db_1 = tempfile.mkstemp()
    db_2_fd, db_2 = tempfile.mkstemp()
    class TestingConfig(Config):
        DEBUG = True
        TESTING = True
        SQLALCHEMY_BINDS = {
            'db1': 'sqlite:///' + db_1,
            'db2': 'sqlite:///' + db_2,
        }

    app = backend.create_app(TestingConfig)
    with app.app_context():
        db.create_all(bind=['db1', 'db2'])
    yield app

    os.close(db_1_fd)
    os.close(db_2_fd)
    os.unlink(db_1)
    os.unlink(db_2)

def test_get_user(app):
    with app.app_context():
        # no user exists yet
        user = User.query.first()
        assert user == None

Actual Behavior

It attempts to call apply_driver_hacks with sa_url = None. How can I prevent apply_driver_hacks from being called when SQLALCHEMY_DATABASE_URI is not set?

=============================================================== test session starts ================================================================
platform darwin -- Python 3.7.4, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: <redacted>
collected 1 item                                                                                                                                   

backend/tests/test_<redacted>.py F                                                                                                                [100%]

===================================================================== FAILURES =====================================================================
__________________________________________________________________ test_get_user ___________________________________________________________________

self = <sqlalchemy.util._collections.ScopedRegistry object at 0x11bc48a50>

    def __call__(self):
        key = self.scopefunc()
        try:
>           return self.registry[key]
E           KeyError: 4576705984

../../.virtualenvs/<redacted>-mTJqo3Um/lib/python3.7/site-packages/sqlalchemy/util/_collections.py:1010: KeyError

During handling of the above exception, another exception occurred:

app = <Flask 'backend'>

    def test_get_user(app):
        with app.app_context():
            # no user exists yet
>           user = User.query.first()

backend/tests/test_<redacted>.py:83: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:518: in __get__
    return type.query_class(mapper, session=self.sa.session())
../../.virtualenvs/<redacted>-mTJqo3Um/lib/python3.7/site-packages/sqlalchemy/orm/scoping.py:78: in __call__
    return self.registry()
../../.virtualenvs/<redacted>-mTJqo3Um/lib/python3.7/site-packages/sqlalchemy/util/_collections.py:1012: in __call__
    return self.registry.setdefault(key, self.createfunc())
../../.virtualenvs/<redacted>-mTJqo3Um/lib/python3.7/site-packages/sqlalchemy/orm/session.py:3213: in __call__
    return self.class_(**local_kw)
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:138: in __init__
    bind = options.pop('bind', None) or db.engine
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:929: in engine
    return self.get_engine()
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:948: in get_engine
    return connector.get_engine()
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:559: in get_engine
    options = self.get_options(sa_url, echo)
../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:574: in get_options
    self._sa.apply_driver_hacks(self._app, sa_url, options)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <SQLAlchemy engine=None>, app = <Flask 'backend'>, sa_url = None, options = {}

    def apply_driver_hacks(self, app, sa_url, options):
        """This method is called before engine creation and used to inject
        driver specific hacks into the options.  The `options` parameter is
        a dictionary of keyword arguments that will then be used to call
        the :func:`sqlalchemy.create_engine` function.
    
        The default implementation provides some saner defaults for things
        like pool sizes for MySQL and sqlite.  Also it injects the setting of
        `SQLALCHEMY_NATIVE_UNICODE`.
        """
>       if sa_url.drivername.startswith('mysql'):
E       AttributeError: 'NoneType' object has no attribute 'drivername'

../../.virtualenvs/<redacted>-mTJqo3Um/src/flask-sqlalchemy/flask_sqlalchemy/__init__.py:869: AttributeError

Environment

  • Operating system: macOS
  • Python version: 3.7.4
  • Flask-SQLAlchemy version: using master branch to include changes related to #663
  • SQLAlchemy version: 1.3.4

colegleason avatar May 13 '20 17:05 colegleason

I'd like to see a little more information. How are you choosing a DB in "normal" operation outside of tests? Is it possible to share some model code, with the bind key set?

CoburnJoe avatar Jun 01 '20 11:06 CoburnJoe

Hi, here is an example of a model with the bind key set. All of my models have a bind key set for either one DB or another.

class User(db.Model):
    __bind_key__ = 'twitter'
    id = db.Column(db.String(36), nullable=False, primary_key=True)
    screen_name = db.Column(db.String(36), nullable=True)
    research_team = db.Column(db.Boolean, nullable=False, default=False)
    consent = db.Column(db.Boolean, nullable=False, default=False)
    reminders = db.Column(db.Boolean, nullable=False, default=True)
    sessions = db.relationship("UserSession")
    consent_form = db.relationship("ConsentForm")

colegleason avatar Jun 09 '20 15:06 colegleason

@colegleason I think the quickest way to solve this issue may be to always set a SQLALCHEMY_DATABASE_URI, even if you then choose a bind for every request...

CoburnJoe avatar Feb 23 '21 16:02 CoburnJoe

Should be fixed in #1087, which changed how engines are created and configured.

davidism avatar Sep 18 '22 17:09 davidism