feat: refactor config/extension, session logic per wuttjamaican

this leverages wuttjamaican for a few more things perhaps, but mostly
this is to organize the code in a more logical way based on
conventions used in wuttjamaican.
This commit is contained in:
Lance Edgar 2024-08-15 13:17:38 -05:00
parent a3c9fe09a7
commit 86d0380387
14 changed files with 521 additions and 331 deletions

View file

@ -4,12 +4,6 @@
.. automodule:: rattail.db
.. class:: Session
SQLAlchemy session class for the Rattail database.
See also :class:`sqlalchemy:sqlalchemy.orm.Session`.
.. toctree::
:maxdepth: 1
:caption: Contents:
@ -37,4 +31,5 @@
model.stores
model.users
model.vendors
sess
util

View file

@ -0,0 +1,22 @@
``rattail.db.sess``
===================
.. automodule:: rattail.db.sess
:members:
.. class:: Session
SQLAlchemy session class used for all (normal) :term:`app database`
connections.
This is a subclass of :class:`SessionBase`.
See the :class:`sqlalchemy:sqlalchemy.orm.Session` docs for more
info.
.. note::
WuttJamaican also provides a
:class:`~wuttjamaican:wuttjamaican.db.sess.Session` class,
however for now Rattail still must define/use its own.

View file

@ -83,7 +83,7 @@ rattailw = "rattail.commands.base:rattail_typer"
[project.entry-points."rattail.config.extensions"]
"rattail.db" = "rattail.db:ConfigExtension"
"rattail" = "rattail.config:RattailConfigExtension"
"rattail.trainwreck" = "rattail.trainwreck.config:TrainwreckConfig"

View file

@ -87,13 +87,30 @@ class RattailConfig(WuttaConfig):
kwargs.setdefault('appname', 'rattail')
super().__init__(*args, **kwargs)
if hasattr(self, 'appdb_engine'):
from rattail.db import Session
Session.configure(bind=self.appdb_engine)
# this is false, unless/until it becomes true
self.versioning_has_been_enabled = False
# TODO: deprecate / remove these
if hasattr(self, 'appdb_engines'):
self.rattail_engines = self.appdb_engines
self.rattail_engine = self.appdb_engine
# configure session if applicable
from rattail.db import Session
if Session:
# nb. wutta configures its own Session but we must
# configure ours separately (albeit with same engine)
if hasattr(self, 'appdb_engine'):
Session.configure(bind=self.appdb_engine)
# TODO: eventually this should not be needed (?)
Session.configure(rattail_config=self)
# TODO: this should be removed, it sets 'record changes' globally
from rattail.db.config import configure_session
configure_session(self, Session)
@property
def prioritized_files(self):
"""
@ -530,13 +547,138 @@ class RattailConfig(WuttaConfig):
class ConfigExtension(WuttaConfigExtension):
"""
Base class for all config extensions.
DEPRECATED - use
:class:`wuttjamaican:wuttjamaican.conf.WuttaConfigExtension`
instead.
This is just a compatibility wrapper around
:class:`wuttjamaican:wuttjamaican.conf.WuttaConfigExtension`; new
code should probably use that directly.
See also :class:`RattailConfigExtension`.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
warnings.warn(f"rattail.config.ConfigExtension (for '{self.key}') is deprecated; "
"please use wuttjamaican.conf.ConfigExtension instead",
DeprecationWarning, stacklevel=2)
class RattailConfigExtension(WuttaConfigExtension):
"""
The :term:`config extension` for Rattail.
This is a subclass of
:class:`wuttjamaican:wuttjamaican.conf.WuttaConfigExtension`.
This primarily exists to add default config settings for sake of
the various importers etc. contained in the ``rattail`` package.
.. note::
If you are adding a new config extension, **do not subclass
this**, but rather subclass
:class:`wuttjamaican:wuttjamaican.conf.WuttaConfigExtension`
directly.
"""
key = 'rattail.db'
def configure(self, config):
""" """
# rattail export-csv
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.default_handler',
'rattail.importing.exporters:FromRattailToCSV')
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.default_cmd',
'rattail export-csv')
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.legacy_handler_setting',
'rattail.exporting, csv.handler')
# rattail export-rattail
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.default_handler',
'rattail.importing.rattail:FromRattailToRattailExport')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.default_cmd',
'rattail export-rattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.legacy_handler_setting',
'rattail.exporting, rattail.handler')
# rattail import-csv
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.default_handler',
'rattail.importing.csv:FromCSVToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.default_cmd',
'rattail import-csv')
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.legacy_handler_setting',
'rattail.importing, csv.handler')
# rattail import-ifps
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.default_handler',
'rattail.importing.ifps:FromIFPSToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.default_cmd',
'rattail import-ifps')
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.legacy_handler_setting',
'rattail.importing, ifps.handler')
# rattail import-rattail
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.default_handler',
'rattail.importing.rattail:FromRattailToRattailImport')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.default_cmd',
'rattail import-rattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.legacy_handler_setting',
'rattail.importing, rattail.handler')
# rattail import-rattail-bulk
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.default_handler',
'rattail.importing.rattail_bulk:BulkFromRattailToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.default_cmd',
'rattail import-rattail-bulk')
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.legacy_handler_setting',
'rattail.importing, rattail_bulk.handler')
# rattail import-sample
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.default_handler',
'rattail.importing.sample:FromSampleToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.default_cmd',
'rattail import-sample')
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.legacy_handler_setting',
'rattail.importing, sample.handler')
# rattail import-versions
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.default_handler',
'rattail.importing.versions:FromRattailToRattailVersions')
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.default_cmd',
'rattail import-versions')
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.legacy_handler_setting',
'rattail.importing, versions.handler')
# trainwreck export-trainwreck
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.default_handler',
'rattail.trainwreck.importing.trainwreck:FromTrainwreckToTrainwreckExport')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.default_cmd',
'trainwreck export-trainwreck')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.legacy_handler_setting',
'trainwreck.exporting, trainwreck.handler')
# trainwreck import-self
config.setdefault('rattail.importing', 'to_trainwreck.from_self.import.default_cmd',
'trainwreck import-self')
# trainwreck import-trainwreck
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.default_handler',
'rattail.trainwreck.importing.trainwreck:FromTrainwreckToTrainwreckImport')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.default_cmd',
'trainwreck import-trainwreck')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.legacy_handler_setting',
'trainwreck.importing, trainwreck.handler')
# nb. cannot fetch DB settings during init
appdir = config.appdir(require=False, usedb=False)
if not appdir:
appdir = os.path.join(sys.prefix, 'app')
poser = config.get('rattail', 'poser', usedb=False,
default=os.path.join(appdir, 'poser'))
# add poser to path if it exists
if os.path.isdir(poser) and poser not in sys.path:
sys.path.append(poser)
def rattail_default_files(appname):
"""

View file

@ -24,228 +24,21 @@
Database Stuff
"""
import os
import sys
import logging
import warnings
try:
import sqlalchemy
except ImportError:
sqlalchemy = None
else:
from sqlalchemy import orm
from .sess import Session, SessionBase as TrueSessionBase
from rattail.config import ConfigExtension as BaseExtension
log = logging.getLogger(__name__)
if sqlalchemy:
class SessionBase(orm.Session):
"""
Custom SQLAlchemy session class, which adds some convenience methods
related to the SQLAlchemy-Continuum integration.
"""
def __init__(self, rattail_config=None, rattail_record_changes=None, continuum_user=None, **kwargs):
"""
Custom constructor, to allow specifying the Continuum user at session
creation. If ``continuum_user`` is specified, its value will be passed
to :meth:`set_continuum_user()`.
"""
super().__init__(**kwargs)
self.rattail_config = rattail_config
# maybe record changes
if rattail_record_changes is None:
rattail_record_changes = getattr(self.bind, 'rattail_record_changes', False)
if rattail_record_changes:
from rattail.db.changes import record_changes
record_changes(self, config=self.rattail_config)
else:
self.rattail_record_changes = False
if continuum_user is None:
self.continuum_user = None
else:
self.set_continuum_user(continuum_user)
# maybe log the current db pool status
if getattr(self.bind, 'rattail_log_pool_status', False):
log.debug(self.bind.pool.status())
def set_continuum_user(self, user_info):
"""
Set the effective Continuum user for the session.
:param user_info: May be a :class:`model.User` instance, or the
``uuid`` or ``username`` for one.
"""
if self.rattail_config:
app = self.rattail_config.get_app()
model = app.model
else:
from rattail.db import model
if isinstance(user_info, model.User):
user = self.merge(user_info)
else:
user = self.get(model.User, user_info)
if not user:
try:
user = self.query(model.User).filter_by(username=user_info).one()
except orm.exc.NoResultFound:
user = None
self.continuum_user = user
Session = orm.sessionmaker(class_=SessionBase, rattail_config=None, expire_on_commit=False)
else: # no sqlalchemy
except ImportError: # pragma: no cover
Session = None
SessionBase = None
else:
class ConfigExtension(BaseExtension):
"""
Config extension for the ``rattail.db`` subpackage. This extension is
responsible for loading the available Rattail database engine(s), and
configuring the :class:`Session` class with the default engine. This
extension expects to find something like the following in your config file:
class SessionBase(TrueSessionBase):
.. code-block:: ini
[rattail.db]
keys = default, host, other
default.url = postgresql://localhost/rattail
host.url = postgresql://host-server/rattail
other.url = postgresql://other-server/rattail
The result of this extension's processing is that the config object will
get two new attributes:
.. attribute:: rattail.config.RattailConfig.rattail_engines
Dict of available Rattail database engines. Keys of the dict are the
same as found in the config file; values are the database engines. Note
that it is possible for this to be an empty dict.
.. attribute:: rattail.config.RattailConfig.rattail_engine
Default database engine; same as ``rattail_engines['default']``. Note
that it is possible for this value to be ``None``.
"""
key = 'rattail.db'
def configure(self, config):
if Session:
from rattail.db.config import configure_session
# TODO: deprecate / remove these
config.rattail_engines = config.appdb_engines
config.rattail_engine = config.appdb_engine
# TODO: eventually this should not be needed (?)
Session.configure(rattail_config=config)
# TODO: This should be removed, it sets 'record changes' globally.
configure_session(config, Session)
# rattail export-csv
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.default_handler',
'rattail.importing.exporters:FromRattailToCSV')
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.default_cmd',
'rattail export-csv')
config.setdefault('rattail.importing', 'to_csv.from_rattail.export.legacy_handler_setting',
'rattail.exporting, csv.handler')
# rattail export-rattail
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.default_handler',
'rattail.importing.rattail:FromRattailToRattailExport')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.default_cmd',
'rattail export-rattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.export.legacy_handler_setting',
'rattail.exporting, rattail.handler')
# rattail import-csv
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.default_handler',
'rattail.importing.csv:FromCSVToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.default_cmd',
'rattail import-csv')
config.setdefault('rattail.importing', 'to_rattail.from_csv.import.legacy_handler_setting',
'rattail.importing, csv.handler')
# rattail import-ifps
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.default_handler',
'rattail.importing.ifps:FromIFPSToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.default_cmd',
'rattail import-ifps')
config.setdefault('rattail.importing', 'to_rattail.from_ifps.import.legacy_handler_setting',
'rattail.importing, ifps.handler')
# rattail import-rattail
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.default_handler',
'rattail.importing.rattail:FromRattailToRattailImport')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.default_cmd',
'rattail import-rattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail.import.legacy_handler_setting',
'rattail.importing, rattail.handler')
# rattail import-rattail-bulk
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.default_handler',
'rattail.importing.rattail_bulk:BulkFromRattailToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.default_cmd',
'rattail import-rattail-bulk')
config.setdefault('rattail.importing', 'to_rattail.from_rattail_bulk.import.legacy_handler_setting',
'rattail.importing, rattail_bulk.handler')
# rattail import-sample
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.default_handler',
'rattail.importing.sample:FromSampleToRattail')
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.default_cmd',
'rattail import-sample')
config.setdefault('rattail.importing', 'to_rattail.from_sample.import.legacy_handler_setting',
'rattail.importing, sample.handler')
# rattail import-versions
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.default_handler',
'rattail.importing.versions:FromRattailToRattailVersions')
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.default_cmd',
'rattail import-versions')
config.setdefault('rattail.importing', 'to_rattail_versions.from_rattail.import.legacy_handler_setting',
'rattail.importing, versions.handler')
# trainwreck export-trainwreck
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.default_handler',
'rattail.trainwreck.importing.trainwreck:FromTrainwreckToTrainwreckExport')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.default_cmd',
'trainwreck export-trainwreck')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.export.legacy_handler_setting',
'trainwreck.exporting, trainwreck.handler')
# trainwreck import-self
config.setdefault('rattail.importing', 'to_trainwreck.from_self.import.default_cmd',
'trainwreck import-self')
# trainwreck import-trainwreck
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.default_handler',
'rattail.trainwreck.importing.trainwreck:FromTrainwreckToTrainwreckImport')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.default_cmd',
'trainwreck import-trainwreck')
config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.legacy_handler_setting',
'trainwreck.importing, trainwreck.handler')
# nb. cannot fetch DB settings during init
appdir = config.appdir(require=False, usedb=False)
if not appdir:
appdir = os.path.join(sys.prefix, 'app')
poser = config.get('rattail', 'poser', usedb=False,
default=os.path.join(appdir, 'poser'))
# add poser to path if it exists
if os.path.isdir(poser) and poser not in sys.path:
sys.path.append(poser)
def __init__(self, *args, **kwargs):
warnings.warn("SessionBase should not be imported from rattail.db; "
"please import it from rattail.db.sess instead",
DeprecationWarning, stacklevel=2)
super().__init__(*args, **kwargs)

104
rattail/db/sess.py Normal file
View file

@ -0,0 +1,104 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Database Sessions
"""
import logging
import sqlalchemy
from sqlalchemy import orm
log = logging.getLogger(__name__)
class SessionBase(orm.Session):
"""
Custom SQLAlchemy session base class, which adds some
convenience methods related to the SQLAlchemy-Continuum
integration.
You should not instantiate this class directly; instead just
use :class:`Session`.
:param continuum_user: Optional user for Continuum versioning
authorship. If specified, the value is passed to
:meth:`set_continuum_user()`.
"""
def __init__(
self,
rattail_config=None,
rattail_record_changes=None,
continuum_user=None,
**kwargs,
):
super().__init__(**kwargs)
self.rattail_config = rattail_config
# maybe record changes
if rattail_record_changes is None:
rattail_record_changes = getattr(self.bind, 'rattail_record_changes', False)
if rattail_record_changes:
from rattail.db.changes import record_changes
record_changes(self, config=self.rattail_config)
else:
self.rattail_record_changes = False
if continuum_user is None:
self.continuum_user = None
else:
self.set_continuum_user(continuum_user)
# maybe log the current db pool status
if getattr(self.bind, 'rattail_log_pool_status', False):
log.debug(self.bind.pool.status())
def set_continuum_user(self, user_info):
"""
Set the effective Continuum user for the session.
:param user_info: May be a
:class:`~rattail.db.model.users.User` instance, or the
``uuid`` or ``username`` for one.
"""
if self.rattail_config:
app = self.rattail_config.get_app()
model = app.model
else:
from rattail.db import model
if isinstance(user_info, model.User):
user = self.merge(user_info)
else:
user = self.get(model.User, user_info)
if not user:
try:
user = self.query(model.User).filter_by(username=user_info).one()
except orm.exc.NoResultFound:
user = None
self.continuum_user = user
Session = orm.sessionmaker(class_=SessionBase, rattail_config=None, expire_on_commit=False)

34
rattail/testing.py Normal file
View file

@ -0,0 +1,34 @@
# -*- coding: utf-8; -*-
from wuttjamaican.testing import FileConfigTestCase
from rattail.config import RattailConfig
class DataTestCase(FileConfigTestCase):
"""
Base class for test suites requiring a full (typical) database.
"""
def setUp(self):
self.setup_db()
def setup_db(self):
self.setup_files()
self.config = self.make_config()
self.app = self.config.get_app()
# init db
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
self.session = self.app.make_session()
def tearDown(self):
self.teardown_db()
def teardown_db(self):
self.teardown_files()
def make_config(self):
return RattailConfig(defaults={
'rattail.db.default.url': 'sqlite://',
})

View file

@ -24,15 +24,15 @@
Trainwreck Config
"""
from wuttjamaican.conf import WuttaConfigExtension
try:
from rattail.trainwreck.db import Session as TrainwreckSession
except ImportError:
TrainwreckSession = None
from rattail.config import ConfigExtension
class TrainwreckConfig(ConfigExtension):
class TrainwreckConfig(WuttaConfigExtension):
"""
Configures any Trainwreck database connections
"""

21
tests/db/test___init__.py Normal file
View file

@ -0,0 +1,21 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from rattail import db as mod
class TestModule(TestCase):
def test_basic(self):
try:
from sqlalchemy import orm
except ImportError:
pytest.skip("test is not relevant without sqlalchemy")
# Session is legitimate
self.assertTrue(isinstance(mod.Session, orm.sessionmaker))
# SessionBase warns of deprecation
# (we don't test for that, just here for the coverage)
session = mod.SessionBase()

View file

@ -1,94 +0,0 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from rattail import db
from rattail.config import RattailConfig
from wuttjamaican.testing import FileConfigTestCase
class TestConfigExtension(FileConfigTestCase):
def tearDown(self):
if db.Session:
db.Session.configure(bind=None, rattail_config=None)
self.teardown_files()
def test_configure_empty(self):
config = RattailConfig()
self.assertRaises(AttributeError, getattr, config, 'rattail_engines')
self.assertRaises(AttributeError, getattr, config, 'rattail_engine')
if hasattr(db.Session, 'kw'):
self.assertIsNone(db.Session.kw['bind'])
self.assertIsNone(db.Session.kw['rattail_config'])
if db.Session:
db.ConfigExtension().configure(config)
self.assertEqual(config.rattail_engines, {})
self.assertIsNone(config.rattail_engine)
if hasattr(db.Session, 'kw'):
self.assertIs(db.Session.kw['rattail_config'], config)
def test_configure_connections(self):
default_path = self.write_file('default.sqlite', '')
default_url = f'sqlite:///{default_path}'
host_path = self.write_file('host.sqlite', '')
host_url = f'sqlite:///{host_path}'
config = RattailConfig(defaults={
'rattail.db.keys': 'default, host',
'rattail.db.default.url': default_url,
'rattail.db.host.url': host_url,
})
# nb. possibly running with 'nodb' scenario here
ext = db.ConfigExtension()
ext.configure(config)
if db.Session:
# ensure rattail_engines (deprecated)
self.assertEqual(len(config.rattail_engines), 2)
self.assertEqual(str(config.rattail_engines['default'].url), default_url)
self.assertEqual(str(config.rattail_engines['host'].url), host_url)
self.assertEqual(str(config.rattail_engine.url), default_url)
session = db.Session()
self.assertIs(session.bind, config.appdb_engine)
self.assertIs(session.bind, config.rattail_engine)
try:
import sqlalchemy as sa
except ImportError:
pass
else:
class TestSession(TestCase):
def test_init_rattail_config(self):
db.Session.configure(rattail_config=None)
session = db.Session()
self.assertIsNone(session.rattail_config)
session.close()
config = object()
session = db.Session(rattail_config=config)
self.assertIs(session.rattail_config, config)
session.close()
def test_init_record_changes(self):
if hasattr(db.Session, 'kw'):
self.assertIsNone(db.Session.kw.get('rattail_record_changes'))
session = db.Session()
self.assertFalse(session.rattail_record_changes)
session.close()
session = db.Session(rattail_record_changes=True)
self.assertTrue(session.rattail_record_changes)
session.close()
engine = sa.create_engine('sqlite://')
engine.rattail_record_changes = True
session = db.Session(bind=engine)
self.assertTrue(session.rattail_record_changes)
session.close()

99
tests/db/test_sess.py Normal file
View file

@ -0,0 +1,99 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from rattail import db
from rattail.testing import DataTestCase
try:
import sqlalchemy as sa
from rattail.db import sess as mod
except ImportError:
pass
else:
class TestSession(DataTestCase):
def setUp(self):
self.setup_db()
model = self.app.model
self.user = model.User(username='barney')
self.session.add(self.user)
self.session.commit()
def test_init_rattail_config(self):
mod.Session.configure(rattail_config=None)
session = mod.Session()
self.assertIsNone(session.rattail_config)
session.close()
config = object()
session = mod.Session(rattail_config=config)
self.assertIs(session.rattail_config, config)
session.close()
def test_init_record_changes(self):
if hasattr(mod.Session, 'kw'):
self.assertIsNone(mod.Session.kw.get('rattail_record_changes'))
session = mod.Session()
self.assertFalse(session.rattail_record_changes)
session.close()
session = mod.Session(rattail_record_changes=True)
self.assertTrue(session.rattail_record_changes)
session.close()
engine = sa.create_engine('sqlite://')
engine.rattail_record_changes = True
session = mod.Session(bind=engine)
self.assertTrue(session.rattail_record_changes)
session.close()
def test_continuum_user_param(self):
# null by default
session = mod.Session()
self.assertIsNone(session.continuum_user)
# can pass user
session = mod.Session(continuum_user=self.user)
self.assertEqual(session.continuum_user.uuid, self.user.uuid)
# can pass username
session = mod.Session(continuum_user='barney')
self.assertEqual(session.continuum_user.uuid, self.user.uuid)
def test_log_pool_status(self):
# sanity/coverage check
self.config.appdb_engine.rattail_log_pool_status = True
session = mod.Session()
def test_set_continuum_user(self):
session = mod.Session()
# null by default
self.assertIsNone(session.continuum_user)
# can pass user
session.set_continuum_user(self.user)
self.assertEqual(session.continuum_user.uuid, self.user.uuid)
# can pass username
session.set_continuum_user('barney')
self.assertEqual(session.continuum_user.uuid, self.user.uuid)
# null if bad username
session.set_continuum_user('doesnotexist')
self.assertIsNone(session.continuum_user)
# still works if session has no config
# TODO: not sure why session actually has a config here?
# (guessing it is an artifact of running other tests?)
self.assertIsNotNone(session.rattail_config)
with patch.object(session, 'rattail_config', new=None):
self.assertIsNone(session.rattail_config)
session.set_continuum_user('barney')
self.assertEqual(session.continuum_user.uuid, self.user.uuid)

View file

@ -5,7 +5,8 @@ from unittest.mock import patch
try:
import sqlalchemy as sa
from rattail.db import model, Session, SessionBase
from rattail.db import model, Session
from rattail.db.sess import SessionBase
from rattail.importing import rattail as rattail_importing
from .. import RattailMixin, RattailTestCase
from . import ImporterTester

View file

@ -4,6 +4,7 @@ import configparser
import datetime
import os
import sys
import tempfile
from unittest import TestCase
from unittest.mock import patch
@ -28,14 +29,35 @@ class TestRattailConfig(FileConfigTestCase):
if db.Session:
session = db.Session()
self.assertIsNone(session.bind)
# nb. session also has our config now
self.assertIs(session.rattail_config, config)
else:
# no sqlalchemy, so no rattail engines
self.assertFalse(hasattr(config, 'rattail_engines'))
self.assertFalse(hasattr(config, 'rattail_engine'))
# default db
config = self.make_config(defaults={
'rattail.db.default.url': 'sqlite://',
})
if db.Session:
# nb. "record changes" off by default
self.assertFalse(hasattr(db.Session, 'rattail_record_changes'))
session = db.Session()
self.assertEqual(str(session.bind.url), 'sqlite://')
self.assertFalse(session.rattail_record_changes)
# with "record changes"
config = self.make_config(defaults={
'rattail.db.default.url': 'sqlite://',
'rattail.db.changes.record': 'true',
})
if db.Session:
self.assertTrue(db.Session.rattail_record_changes)
session = db.Session()
# nb. session instance still does not have the flag
# TODO: is this by design..? does it need to improve?
self.assertFalse(session.rattail_record_changes)
def test_prioritized_files(self):
first = self.write_file('first.conf', """\
@ -353,6 +375,44 @@ require = %(here)s/first.conf
self.assertEqual(myconfig.batch_filedir(key='bar'), os.path.join(path, 'bar'))
class TestLegacyConfigExtensionBase(TestCase):
def test_basic(self):
# sanity / coverage check
ext = mod.ConfigExtension()
class TestRattailConfigExtension(TestCase):
def make_config(self, **kwargs):
return mod.RattailConfig(**kwargs)
def make_extension(self):
return mod.RattailConfigExtension()
def test_configure(self):
# no import config yet
config = self.make_config()
self.assertEqual(config.defaults, {})
self.assertIsNone(config.get('rattail.importing.to_rattail.from_csv.import.default_handler'))
# extension adds import config
ext = self.make_extension()
ext.configure(config)
spec = config.get('rattail.importing.to_rattail.from_csv.import.default_handler')
self.assertIsNotNone(spec)
# poser dir added to path
self.assertNotIn('/tmp/foo', sys.path)
tempdir = tempfile.mkdtemp()
config.setdefault('rattail.poser', tempdir)
ext.configure(config)
self.assertIn(tempdir, sys.path)
sys.path.remove(tempdir)
os.rmdir(tempdir)
class TestRattailDefaultFiles(FileConfigTestCase):
def test_quiet_conf(self):

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8; -*-
from rattail.trainwreck import config as mod
from rattail.testing import DataTestCase
class TestTrainwreckConfig(DataTestCase):
def test_configure(self):
self.assertFalse(hasattr(self.config, 'trainwreck_engines'))
ext = mod.TrainwreckConfig()
ext.configure(self.config)
self.assertEqual(self.config.trainwreck_engines, {})