fix: ensure config has no app when constructor finishes
had to move `make_engine_from_config()` out of app handler and define as a separate function, so that `get_engines()` did not need to instantiate the app handler. because if it did, then config extensions would lose the ability to set a default app handler - er, they could do it but it would be ignored
This commit is contained in:
parent
3ab181b129
commit
c3efbfbf7b
|
@ -142,63 +142,6 @@ class AppHandler:
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
|
|
||||||
def make_engine_from_config(
|
|
||||||
self,
|
|
||||||
config_dict,
|
|
||||||
prefix='sqlalchemy.',
|
|
||||||
**kwargs):
|
|
||||||
"""
|
|
||||||
Construct a new DB engine from configuration dict.
|
|
||||||
|
|
||||||
This is a wrapper around upstream
|
|
||||||
:func:`sqlalchemy:sqlalchemy.engine_from_config()`. For even
|
|
||||||
broader context of the SQLAlchemy
|
|
||||||
:class:`~sqlalchemy:sqlalchemy.engine.Engine` and their
|
|
||||||
configuration, see :doc:`sqlalchemy:core/engines`.
|
|
||||||
|
|
||||||
The purpose of the customization is to allow certain
|
|
||||||
attributes of the engine to be driven by config, whereas the
|
|
||||||
upstream function is more limited in that regard. The
|
|
||||||
following in particular:
|
|
||||||
|
|
||||||
* ``poolclass``
|
|
||||||
* ``pool_pre_ping``
|
|
||||||
|
|
||||||
If these options are present in the configuration dict, they
|
|
||||||
will be coerced to appropriate Python equivalents and then
|
|
||||||
passed as kwargs to the upstream function.
|
|
||||||
|
|
||||||
An example config file leveraging this feature:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[wutta.db]
|
|
||||||
default.url = sqlite:///tmp/default.sqlite
|
|
||||||
default.poolclass = sqlalchemy.pool:NullPool
|
|
||||||
default.pool_pre_ping = true
|
|
||||||
|
|
||||||
Note that if present, the ``poolclass`` value must be a "spec"
|
|
||||||
string, as required by
|
|
||||||
:func:`~wuttjamaican.util.load_object()`.
|
|
||||||
"""
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
config_dict = dict(config_dict)
|
|
||||||
|
|
||||||
# convert 'poolclass' arg to actual class
|
|
||||||
key = f'{prefix}poolclass'
|
|
||||||
if key in config_dict and 'poolclass' not in kwargs:
|
|
||||||
kwargs['poolclass'] = load_object(config_dict.pop(key))
|
|
||||||
|
|
||||||
# convert 'pool_pre_ping' arg to boolean
|
|
||||||
key = f'{prefix}pool_pre_ping'
|
|
||||||
if key in config_dict and 'pool_pre_ping' not in kwargs:
|
|
||||||
kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key))
|
|
||||||
|
|
||||||
engine = sa.engine_from_config(config_dict, prefix, **kwargs)
|
|
||||||
|
|
||||||
return engine
|
|
||||||
|
|
||||||
def make_session(self, **kwargs):
|
def make_session(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a new SQLAlchemy session for the app DB. By default
|
Creates a new SQLAlchemy session for the app DB. By default
|
||||||
|
|
|
@ -570,12 +570,12 @@ class WuttaConfig:
|
||||||
|
|
||||||
See also :doc:`/narr/handlers/app`.
|
See also :doc:`/narr/handlers/app`.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, 'app'):
|
if not hasattr(self, '_app'):
|
||||||
spec = self.get(f'{self.appname}.app.handler', usedb=False,
|
spec = self.get(f'{self.appname}.app.handler', usedb=False,
|
||||||
default='wuttjamaican.app:AppHandler')
|
default='wuttjamaican.app:AppHandler')
|
||||||
factory = load_object(spec)
|
factory = load_object(spec)
|
||||||
self.app = factory(self)
|
self._app = factory(self)
|
||||||
return self.app
|
return self._app
|
||||||
|
|
||||||
|
|
||||||
class WuttaConfigExtension:
|
class WuttaConfigExtension:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttJamaican -- Base package for Wutta Framework
|
# WuttJamaican -- Base package for Wutta Framework
|
||||||
# Copyright © 2023 Lance Edgar
|
# Copyright © 2023-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -28,7 +28,7 @@ from collections import OrderedDict
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from wuttjamaican.util import parse_list
|
from wuttjamaican.util import load_object, parse_bool, parse_list
|
||||||
|
|
||||||
|
|
||||||
def get_engines(config, prefix):
|
def get_engines(config, prefix):
|
||||||
|
@ -62,8 +62,6 @@ def get_engines(config, prefix):
|
||||||
:returns: A dictionary of SQLAlchemy engines, with keys matching
|
:returns: A dictionary of SQLAlchemy engines, with keys matching
|
||||||
those found in config.
|
those found in config.
|
||||||
"""
|
"""
|
||||||
app = config.get_app()
|
|
||||||
|
|
||||||
keys = config.get(f'{prefix}.keys', usedb=False)
|
keys = config.get(f'{prefix}.keys', usedb=False)
|
||||||
if keys:
|
if keys:
|
||||||
keys = parse_list(keys)
|
keys = parse_list(keys)
|
||||||
|
@ -75,11 +73,11 @@ def get_engines(config, prefix):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
try:
|
try:
|
||||||
engines[key] = app.make_engine_from_config(cfg, prefix=f'{key}.')
|
engines[key] = make_engine_from_config(cfg, prefix=f'{key}.')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if key == 'default':
|
if key == 'default':
|
||||||
try:
|
try:
|
||||||
engines[key] = app.make_engine_from_config(cfg, prefix='sqlalchemy.')
|
engines[key] = make_engine_from_config(cfg, prefix='sqlalchemy.')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return engines
|
return engines
|
||||||
|
@ -100,3 +98,56 @@ def get_setting(session, name):
|
||||||
"""
|
"""
|
||||||
sql = sa.text("select value from setting where name = :name")
|
sql = sa.text("select value from setting where name = :name")
|
||||||
return session.execute(sql, params={'name': name}).scalar()
|
return session.execute(sql, params={'name': name}).scalar()
|
||||||
|
|
||||||
|
|
||||||
|
def make_engine_from_config(
|
||||||
|
config_dict,
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
Construct a new DB engine from configuration dict.
|
||||||
|
|
||||||
|
This is a wrapper around upstream
|
||||||
|
:func:`sqlalchemy:sqlalchemy.engine_from_config()`. For even
|
||||||
|
broader context of the SQLAlchemy
|
||||||
|
:class:`~sqlalchemy:sqlalchemy.engine.Engine` and their
|
||||||
|
configuration, see :doc:`sqlalchemy:core/engines`.
|
||||||
|
|
||||||
|
The purpose of the customization is to allow certain attributes of
|
||||||
|
the engine to be driven by config, whereas the upstream function
|
||||||
|
is more limited in that regard. The following in particular:
|
||||||
|
|
||||||
|
* ``poolclass``
|
||||||
|
* ``pool_pre_ping``
|
||||||
|
|
||||||
|
If these options are present in the configuration dict, they will
|
||||||
|
be coerced to appropriate Python equivalents and then passed as
|
||||||
|
kwargs to the upstream function.
|
||||||
|
|
||||||
|
An example config file leveraging this feature:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[wutta.db]
|
||||||
|
default.url = sqlite:///tmp/default.sqlite
|
||||||
|
default.poolclass = sqlalchemy.pool:NullPool
|
||||||
|
default.pool_pre_ping = true
|
||||||
|
|
||||||
|
Note that if present, the ``poolclass`` value must be a "spec"
|
||||||
|
string, as required by :func:`~wuttjamaican.util.load_object()`.
|
||||||
|
"""
|
||||||
|
config_dict = dict(config_dict)
|
||||||
|
|
||||||
|
# convert 'poolclass' arg to actual class
|
||||||
|
key = f'{prefix}poolclass'
|
||||||
|
if key in config_dict and 'poolclass' not in kwargs:
|
||||||
|
kwargs['poolclass'] = load_object(config_dict.pop(key))
|
||||||
|
|
||||||
|
# convert 'pool_pre_ping' arg to boolean
|
||||||
|
key = f'{prefix}pool_pre_ping'
|
||||||
|
if key in config_dict and 'pool_pre_ping' not in kwargs:
|
||||||
|
kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key))
|
||||||
|
|
||||||
|
engine = sa.engine_from_config(config_dict, prefix, **kwargs)
|
||||||
|
|
||||||
|
return engine
|
||||||
|
|
|
@ -7,6 +7,8 @@ from unittest import TestCase
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
from wuttjamaican.db import conf
|
from wuttjamaican.db import conf
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
|
@ -93,3 +95,38 @@ class TestGetSetting(TestCase):
|
||||||
def test_missing_value(self):
|
def test_missing_value(self):
|
||||||
value = conf.get_setting(self.session, 'foo')
|
value = conf.get_setting(self.session, 'foo')
|
||||||
self.assertIsNone(value)
|
self.assertIsNone(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakeEngineFromConfig(TestCase):
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
engine = conf.make_engine_from_config({
|
||||||
|
'sqlalchemy.url': 'sqlite://',
|
||||||
|
})
|
||||||
|
self.assertIsInstance(engine, Engine)
|
||||||
|
|
||||||
|
def test_poolclass(self):
|
||||||
|
|
||||||
|
engine = conf.make_engine_from_config({
|
||||||
|
'sqlalchemy.url': 'sqlite://',
|
||||||
|
})
|
||||||
|
self.assertNotIsInstance(engine.pool, NullPool)
|
||||||
|
|
||||||
|
engine = conf.make_engine_from_config({
|
||||||
|
'sqlalchemy.url': 'sqlite://',
|
||||||
|
'sqlalchemy.poolclass': 'sqlalchemy.pool:NullPool',
|
||||||
|
})
|
||||||
|
self.assertIsInstance(engine.pool, NullPool)
|
||||||
|
|
||||||
|
def test_pool_pre_ping(self):
|
||||||
|
|
||||||
|
engine = conf.make_engine_from_config({
|
||||||
|
'sqlalchemy.url': 'sqlite://',
|
||||||
|
})
|
||||||
|
self.assertFalse(engine.pool._pre_ping)
|
||||||
|
|
||||||
|
engine = conf.make_engine_from_config({
|
||||||
|
'sqlalchemy.url': 'sqlite://',
|
||||||
|
'sqlalchemy.pool_pre_ping': 'true',
|
||||||
|
})
|
||||||
|
self.assertTrue(engine.pool._pre_ping)
|
||||||
|
|
|
@ -9,8 +9,6 @@ from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.engine import Engine
|
|
||||||
from sqlalchemy.pool import NullPool
|
|
||||||
|
|
||||||
from wuttjamaican import app, db
|
from wuttjamaican import app, db
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
|
@ -47,38 +45,6 @@ class TestAppHandler(TestCase):
|
||||||
self.assertEqual(len(os.listdir(tempdir)), 3)
|
self.assertEqual(len(os.listdir(tempdir)), 3)
|
||||||
shutil.rmtree(tempdir)
|
shutil.rmtree(tempdir)
|
||||||
|
|
||||||
def test_make_engine_from_config_basic(self):
|
|
||||||
engine = self.app.make_engine_from_config({
|
|
||||||
'sqlalchemy.url': 'sqlite://',
|
|
||||||
})
|
|
||||||
self.assertIsInstance(engine, Engine)
|
|
||||||
|
|
||||||
def test_make_engine_from_config_poolclass(self):
|
|
||||||
|
|
||||||
engine = self.app.make_engine_from_config({
|
|
||||||
'sqlalchemy.url': 'sqlite://',
|
|
||||||
})
|
|
||||||
self.assertNotIsInstance(engine.pool, NullPool)
|
|
||||||
|
|
||||||
engine = self.app.make_engine_from_config({
|
|
||||||
'sqlalchemy.url': 'sqlite://',
|
|
||||||
'sqlalchemy.poolclass': 'sqlalchemy.pool:NullPool',
|
|
||||||
})
|
|
||||||
self.assertIsInstance(engine.pool, NullPool)
|
|
||||||
|
|
||||||
def test_make_engine_from_config_pool_pre_ping(self):
|
|
||||||
|
|
||||||
engine = self.app.make_engine_from_config({
|
|
||||||
'sqlalchemy.url': 'sqlite://',
|
|
||||||
})
|
|
||||||
self.assertFalse(engine.pool._pre_ping)
|
|
||||||
|
|
||||||
engine = self.app.make_engine_from_config({
|
|
||||||
'sqlalchemy.url': 'sqlite://',
|
|
||||||
'sqlalchemy.pool_pre_ping': 'true',
|
|
||||||
})
|
|
||||||
self.assertTrue(engine.pool._pre_ping)
|
|
||||||
|
|
||||||
def test_make_session(self):
|
def test_make_session(self):
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
self.assertIsInstance(session, db.Session.class_)
|
self.assertIsInstance(session, db.Session.class_)
|
||||||
|
@ -118,7 +84,7 @@ class TestAppProvider(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = WuttaConfig(appname='wuttatest')
|
self.config = WuttaConfig(appname='wuttatest')
|
||||||
self.app = app.AppHandler(self.config)
|
self.app = app.AppHandler(self.config)
|
||||||
self.config.app = self.app
|
self.config._app = self.app
|
||||||
|
|
||||||
def test_constructor(self):
|
def test_constructor(self):
|
||||||
|
|
||||||
|
@ -198,7 +164,7 @@ class TestGenericHandler(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = WuttaConfig(appname='wuttatest')
|
self.config = WuttaConfig(appname='wuttatest')
|
||||||
self.app = app.AppHandler(self.config)
|
self.app = app.AppHandler(self.config)
|
||||||
self.config.app = self.app
|
self.config._app = self.app
|
||||||
|
|
||||||
def test_constructor(self):
|
def test_constructor(self):
|
||||||
handler = app.GenericHandler(self.config)
|
handler = app.GenericHandler(self.config)
|
||||||
|
|
|
@ -228,6 +228,17 @@ configure_logging = true
|
||||||
config = conf.WuttaConfig(files=[myfile])
|
config = conf.WuttaConfig(files=[myfile])
|
||||||
logging.config.fileConfig.assert_called_once()
|
logging.config.fileConfig.assert_called_once()
|
||||||
|
|
||||||
|
def test_config_has_no_app_after_init(self):
|
||||||
|
|
||||||
|
# initial config should *not* have an app yet, otherwise
|
||||||
|
# extensions cannot specify a default app handler
|
||||||
|
config = conf.WuttaConfig()
|
||||||
|
self.assertFalse(hasattr(config, '_app'))
|
||||||
|
|
||||||
|
# but after that we can get an app okay
|
||||||
|
app = config.get_app()
|
||||||
|
self.assertIs(app, config._app)
|
||||||
|
|
||||||
def test_setdefault(self):
|
def test_setdefault(self):
|
||||||
config = conf.WuttaConfig()
|
config = conf.WuttaConfig()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue