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):
|
||||
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):
|
||||
"""
|
||||
Creates a new SQLAlchemy session for the app DB. By default
|
||||
|
|
|
@ -570,12 +570,12 @@ class WuttaConfig:
|
|||
|
||||
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,
|
||||
default='wuttjamaican.app:AppHandler')
|
||||
factory = load_object(spec)
|
||||
self.app = factory(self)
|
||||
return self.app
|
||||
self._app = factory(self)
|
||||
return self._app
|
||||
|
||||
|
||||
class WuttaConfigExtension:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023 Lance Edgar
|
||||
# Copyright © 2023-2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
|
@ -28,7 +28,7 @@ from collections import OrderedDict
|
|||
|
||||
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):
|
||||
|
@ -62,8 +62,6 @@ def get_engines(config, prefix):
|
|||
:returns: A dictionary of SQLAlchemy engines, with keys matching
|
||||
those found in config.
|
||||
"""
|
||||
app = config.get_app()
|
||||
|
||||
keys = config.get(f'{prefix}.keys', usedb=False)
|
||||
if keys:
|
||||
keys = parse_list(keys)
|
||||
|
@ -75,11 +73,11 @@ def get_engines(config, prefix):
|
|||
for key in keys:
|
||||
key = key.strip()
|
||||
try:
|
||||
engines[key] = app.make_engine_from_config(cfg, prefix=f'{key}.')
|
||||
engines[key] = make_engine_from_config(cfg, prefix=f'{key}.')
|
||||
except KeyError:
|
||||
if key == 'default':
|
||||
try:
|
||||
engines[key] = app.make_engine_from_config(cfg, prefix='sqlalchemy.')
|
||||
engines[key] = make_engine_from_config(cfg, prefix='sqlalchemy.')
|
||||
except KeyError:
|
||||
pass
|
||||
return engines
|
||||
|
@ -100,3 +98,56 @@ def get_setting(session, name):
|
|||
"""
|
||||
sql = sa.text("select value from setting where name = :name")
|
||||
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
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from wuttjamaican.db import conf
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
|
@ -93,3 +95,38 @@ class TestGetSetting(TestCase):
|
|||
def test_missing_value(self):
|
||||
value = conf.get_setting(self.session, 'foo')
|
||||
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
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from wuttjamaican import app, db
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
|
@ -47,38 +45,6 @@ class TestAppHandler(TestCase):
|
|||
self.assertEqual(len(os.listdir(tempdir)), 3)
|
||||
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):
|
||||
session = self.app.make_session()
|
||||
self.assertIsInstance(session, db.Session.class_)
|
||||
|
@ -118,7 +84,7 @@ class TestAppProvider(TestCase):
|
|||
def setUp(self):
|
||||
self.config = WuttaConfig(appname='wuttatest')
|
||||
self.app = app.AppHandler(self.config)
|
||||
self.config.app = self.app
|
||||
self.config._app = self.app
|
||||
|
||||
def test_constructor(self):
|
||||
|
||||
|
@ -198,7 +164,7 @@ class TestGenericHandler(TestCase):
|
|||
def setUp(self):
|
||||
self.config = WuttaConfig(appname='wuttatest')
|
||||
self.app = app.AppHandler(self.config)
|
||||
self.config.app = self.app
|
||||
self.config._app = self.app
|
||||
|
||||
def test_constructor(self):
|
||||
handler = app.GenericHandler(self.config)
|
||||
|
|
|
@ -228,6 +228,17 @@ configure_logging = true
|
|||
config = conf.WuttaConfig(files=[myfile])
|
||||
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):
|
||||
config = conf.WuttaConfig()
|
||||
|
||||
|
|
Loading…
Reference in a new issue