test: improve some tests, docs for rattail.config module

still some left to do, just a stopping point
This commit is contained in:
Lance Edgar 2024-07-01 23:07:05 -05:00
parent 937f54681f
commit c5c8c06ea7
7 changed files with 329 additions and 221 deletions

View file

@ -27,6 +27,7 @@
model.batch.product
model.batch.purchase
model.batch.vendorcatalog
model.core
model.customers
model.datasync
model.labels

View file

@ -0,0 +1,33 @@
``rattail.db.model.core``
=========================
.. automodule:: rattail.db.model.core
.. autoclass:: ModelBase
.. attribute:: model_title
Optionally set this to a "humanized" version of the model name, for
display in templates etc. Default value will be guessed from the model
class name, e.g. 'Product' => "Products" and 'CustomerOrder' => "Customer
Order".
.. attribute:: model_title_plural
Optionally set this to a "humanized" version of the *plural* model name,
for display in templates etc. Default value will be guessed from the
model class name, e.g. 'Product' => "Products" and 'CustomerOrder' =>
"Customer Orders".
.. autoclass:: Setting
:members:
.. autoclass:: Change
:members:
.. automodule:: rattail.db.model.contact
:members:
.. todo::
Geez a lot of work left here...

View file

@ -2,32 +2,4 @@
``rattail.db.model``
====================
.. automodule:: rattail.db.model.core
.. autoclass:: ModelBase
.. attribute:: model_title
Optionally set this to a "humanized" version of the model name, for
display in templates etc. Default value will be guessed from the model
class name, e.g. 'Product' => "Products" and 'CustomerOrder' => "Customer
Order".
.. attribute:: model_title_plural
Optionally set this to a "humanized" version of the *plural* model name,
for display in templates etc. Default value will be guessed from the
model class name, e.g. 'Product' => "Products" and 'CustomerOrder' =>
"Customer Orders".
.. autoclass:: Setting
:members:
.. autoclass:: Change
:members:
.. automodule:: rattail.db.model.contact
:members:
.. todo::
Geez a lot of work left here...
.. automodule:: rattail.db.model

View file

@ -2,7 +2,7 @@
try:
from importlib.metadata import version
except ImportError:
except ImportError: # pragma: no cover
from importlib_metadata import version

View file

@ -36,6 +36,7 @@ from wuttjamaican.conf import (WuttaConfig, WuttaConfigExtension,
generic_default_files)
from wuttjamaican.util import (parse_bool as wutta_parse_bool,
parse_list as wutta_parse_list)
from wuttjamaican.exc import ConfigurationError as WuttaConfigurationError
from rattail.exceptions import WindowsExtensionsNotInstalled, ConfigurationError
@ -44,13 +45,7 @@ log = logging.getLogger(__name__)
def parse_bool(value): # pragma: no cover
"""
Compatibility wrapper for
:func:`wuttjamaican:wuttjamaican.util.parse_bool()`.
This function is deprecated; new code should use the upstream
function instead.
"""
""" """
warnings.warn("rattail.config.parse_bool() is deprecated; "
"please use wuttjamaican.util.parse_bool() instead",
DeprecationWarning, stacklevel=2)
@ -58,13 +53,7 @@ def parse_bool(value): # pragma: no cover
def parse_list(value): # pragma: no cover
"""
Compatibility wrapper for
:func:`wuttjamaican:wuttjamaican.util.parse_list()`.
This function is deprecated; new code should use the upstream
function instead.
"""
""" """
warnings.warn("rattail.config.parse_list() is deprecated; "
"please use wuttjamaican.util.parse_list() instead",
DeprecationWarning, stacklevel=2)
@ -120,7 +109,8 @@ class RattailConfig(WuttaConfig):
normally expects just ``(key, value)`` args, but we also (for
now) support the older style of ``(section, option, value)`` -
*eventually* that will go away but probably not in the near
future.
future. However new code should pass ``(key, value)`` since
that is now the preferred signature.
"""
# figure out what sort of args were passed
if len(args) == 2:
@ -143,7 +133,8 @@ class RattailConfig(WuttaConfig):
normally expects just ``(key, ...)`` args, but we also (for
now) support the older style of ``(section, option, ...)`` -
*eventually* that will go away but probably not in the near
future.
future. However new code should pass ``(key, ...)`` since
that is now the preferred signature.
"""
# figure out what sort of args were passed
if len(args) == 1:
@ -171,6 +162,30 @@ class RattailConfig(WuttaConfig):
# DeprecationWarning, stacklevel=2)
return self.get_bool(*args, **kwargs)
def get_date(self, *args, **kwargs):
"""
Retrieve a date value from config.
Accepts same params as :meth:`get()` but if a value is found,
it will be coerced to date via
:meth:`rattail.app.AppHandler.parse_date()`.
"""
value = self.get(*args, **kwargs)
app = self.get_app()
return app.parse_date(value)
def getdate(self, *args, **kwargs):
"""
Backward-compatible alias for :meth:`get_date()`.
New code should use ``get_date()`` instead of this method.
"""
# TODO: eventually
# warnings.warn("config.getbool() method is deprecated; "
# "please use config.get_bool() instead",
# DeprecationWarning, stacklevel=2)
return self.get_date(*args, **kwargs)
def getint(self, *args, **kwargs):
"""
Backward-compatible alias for
@ -201,30 +216,14 @@ class RattailConfig(WuttaConfig):
"""
Convenience method around the
:func:`~wuttjamaican:wuttjamaican.util.parse_bool()` function.
Usage of this method is discouraged, at least until some more
dust settles. This probably belongs on the app handler
instead so it can be overridden more easily.
"""
# TODO: eventually
# warnings.warn("config.parse_bool() method is deprecated; "
# "please use app.parse_bool() instead",
# DeprecationWarning, stacklevel=2)
return wutta_parse_bool(value)
def parse_list(self, value):
"""
Convenience method around the
:func:`~wuttjamaican:wuttjamaican.util.parse_list()` function.
Usage of this method is discouraged, at least until some more
dust settles. This probably belongs on the app handler
instead so it can be overridden more easily.
"""
# TODO: eventually
# warnings.warn("config.parse_list() method is deprecated; "
# "please use app.parse_list() instead",
# DeprecationWarning, stacklevel=2)
return wutta_parse_list(value)
def make_list_string(self, values):
@ -254,7 +253,7 @@ class RattailConfig(WuttaConfig):
def beaker_invalidate_setting(self, name):
"""
Backward-compatible method for unused Beaker caching logic.
Deprecated method for unused Beaker caching logic.
This method has no effect and should not be used.
"""
@ -262,18 +261,6 @@ class RattailConfig(WuttaConfig):
# warnings.warn("config.beaker_invalidate_setting() method is deprecated",
# DeprecationWarning, stacklevel=2)
def node_type(self, default=None):
"""
Returns the "type" of current node. What this means will
generally depend on the app logic.
"""
try:
return self.require('rattail', 'node_type', usedb=False)
except ConfigurationError:
if default:
return default
raise
def production(self):
"""
Returns boolean indicating whether the app is running in
@ -281,10 +268,23 @@ class RattailConfig(WuttaConfig):
"""
return self.getbool('rattail', 'production', default=False)
def node_type(self, default=None):
"""
Returns the "type" of current node as string. What this means
will generally depend on the app logic. There is no default
node type unless caller provides one.
"""
try:
return self.require('rattail', 'node_type', usedb=False)
except WuttaConfigurationError:
if default:
return default
raise
def get_model(self):
"""
Returns a reference to configured 'model' module; defaults to
:mod:`rattail.db.model`.
Returns a reference to the module containing data models;
defaults to :mod:`rattail.db.model`.
"""
spec = self.get('rattail', 'model', usedb=False,
default='rattail.db.model')
@ -303,7 +303,8 @@ class RattailConfig(WuttaConfig):
"""
Returns a reference to the configured data 'model' module for
Trainwreck. Note that there is *not* a default value for
this; it must be configured.
this; it must be configured or else calling this method will
result in an error.
"""
spec = self.require('rattail.trainwreck', 'model', usedb=False)
return importlib.import_module(spec)
@ -315,29 +316,15 @@ class RattailConfig(WuttaConfig):
return self.getbool('rattail.db', 'versioning.enabled', usedb=False,
default=False)
def getdate(self, *args, **kwargs):
"""
Retrieve a date value from config.
"""
value = self.get(*args, **kwargs)
app = self.get_app()
return app.parse_date(value)
def product_key(self, **kwargs):
"""
Deprecated; instead please see
:meth:`rattail.app.AppHandler.get_product_key_field()`.
"""
def product_key(self, **kwargs): # pragma: no cover
""" """
warnings.warn("config.product_key() is deprecated; please "
"use app.get_product_key_field() instead",
DeprecationWarning, stacklevel=2)
return self.get_app().get_product_key_field()
def product_key_title(self, key=None):
"""
Deprecated; instead please see
:meth:`rattail.app.AppHandler.get_product_key_label()`.
"""
def product_key_title(self, key=None): # pragma: no cover
""" """
warnings.warn("config.product_key_title() is deprecated; please "
"use app.get_product_key_label() instead",
DeprecationWarning, stacklevel=2)
@ -352,14 +339,20 @@ class RattailConfig(WuttaConfig):
return self.get('rattail', 'app_package', default=default)
def app_title(self, **kwargs):
""" DEPRECATED """
"""
DEPRECATED; use :meth:`rattail.app.AppHandler.get_title()`
instead.
"""
# TODO: should put a deprecation warning here, but it could
# make things noisy for a while and i'm not ready for that
app = self.get_app()
return app.get_title(**kwargs)
def node_title(self, **kwargs):
""" DEPRECATED """
"""
DEPRECATED; use
:meth:`rattail.app.AppHandler.get_node_title()` instead.
"""
# TODO: should put a deprecation warning here, but it could
# make things noisy for a while and i'm not ready for that
app = self.get_app()
@ -369,18 +362,28 @@ class RattailConfig(WuttaConfig):
"""
Returns boolean indicating whether the app is running from
source, as opposed to official release.
.. warning::
The utility of this method is questionable and it ideally
will go away in the future.
"""
return self.getbool('rattail', 'running_from_source', default=False)
def demo(self):
"""
Returns boolean indicating whether the app is running in demo mode
.. warning::
The utility of this method is questionable and it ideally
will go away in the future.
"""
return self.getbool('rattail', 'demo', default=False)
def appdir(self, require=True, **kwargs):
"""
Returns path to the 'app' dir, if known.
Returns path to the local 'app' dir.
"""
if require:
path = os.path.join(sys.prefix, 'app')
@ -405,7 +408,7 @@ class RattailConfig(WuttaConfig):
def batch_filedir(self, key=None):
"""
Returns path to root folder where batches (optionally of type
'key') are stored.
``key``) are stored.
"""
path = os.path.abspath(self.require('rattail', 'batch.files'))
if key:

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -22,6 +22,9 @@
################################################################################
"""
Rattail data models
This namespace will be populated with all data model classes defined
by Rattail.
"""
from .core import Base, ModelBase, uuid_column, getset_factory, GPCType, Setting, Change, Note

View file

@ -3,120 +3,17 @@
import configparser
import datetime
import os
import sys
from unittest import TestCase
from unittest.mock import patch
from wuttjamaican.testing import FileConfigTestCase
from wuttjamaican.exc import ConfigurationError
from rattail import config
from rattail.app import AppHandler
class TestRattailConfig(FileConfigTestCase):
def setup_files(self):
self.site_path = self.write_file('site.conf', """
[rattail]
""")
self.host_path = self.write_file('host.conf', """
[rattail.config]
include = "{}"
""".format(self.site_path))
self.app_path = self.write_file('app.conf', """
[rattail.config]
include = "{}"
""".format(self.host_path))
self.custom_path = self.write_file('custom.conf', """
[rattail.config]
include = "%(here)s/app.conf"
""")
def test_init_defaults(self):
cfg = config.RattailConfig()
self.assertEqual(cfg.files_requested, [])
self.assertEqual(cfg.files_read, [])
def test_init_params(self):
self.setup_files()
# files
cfg = config.RattailConfig()
self.assertEqual(cfg.files_requested, [])
self.assertEqual(cfg.files_read, [])
cfg = config.RattailConfig(files=[self.site_path])
self.assertEqual(cfg.files_requested, [self.site_path])
self.assertEqual(cfg.files_read, [self.site_path])
# usedb
cfg = config.RattailConfig()
self.assertFalse(cfg.usedb)
cfg = config.RattailConfig(usedb=True)
self.assertTrue(cfg.usedb)
# preferdb
cfg = config.RattailConfig()
self.assertFalse(cfg.preferdb)
cfg = config.RattailConfig(preferdb=True)
self.assertTrue(cfg.preferdb)
def test_read_file_with_recurse(self):
self.setup_files()
cfg = config.RattailConfig()
cfg.read_file(self.custom_path, recurse=True)
self.assertEqual(cfg.files_requested, [self.custom_path, self.app_path, self.host_path, self.site_path])
self.assertEqual(cfg.files_read, [self.site_path, self.host_path, self.app_path, self.custom_path])
def test_read_file_once_only(self):
self.setup_files()
another_path = self.write_file('another.conf', """
[rattail.config]
include = "{custom}" "{site}" "{app}" "{site}" "{custom}"
""".format(custom=self.custom_path, app=self.app_path, site=self.site_path))
cfg = config.RattailConfig()
cfg.read_file(another_path, recurse=True)
self.assertEqual(cfg.files_requested, [another_path, self.custom_path, self.app_path, self.host_path, self.site_path])
self.assertEqual(cfg.files_read, [self.site_path, self.host_path, self.app_path, self.custom_path, another_path])
def test_read_file_skip_missing(self):
self.setup_files()
bogus_path = '/tmp/does-not/exist'
self.assertFalse(os.path.exists(bogus_path))
another_path = self.write_file('another.conf', """
[rattail.config]
include = "{bogus}" "{app}" "{bogus}" "{site}"
""".format(bogus=bogus_path, app=self.app_path, site=self.site_path))
cfg = config.RattailConfig()
cfg.read_file(another_path, recurse=True)
self.assertEqual(cfg.files_requested, [another_path, bogus_path, self.app_path, self.host_path, self.site_path])
self.assertEqual(cfg.files_read, [self.site_path, self.host_path, self.app_path, another_path])
@patch('rattail.config.logging.config.fileConfig')
def test_configure_logging(self, fileConfig):
cfg = config.RattailConfig()
# logging not configured by default
cfg.configure_logging()
self.assertFalse(fileConfig.called)
# but config option can enable it
cfg.set('rattail.config', 'configure_logging', 'true')
cfg.configure_logging()
self.assertEqual(fileConfig.call_count, 1)
fileConfig.reset_mock()
# invalid logging config is ignored
fileConfig.side_effect = configparser.NoSectionError('loggers')
cfg.configure_logging()
self.assertEqual(fileConfig.call_count, 1)
class TestRattailConfig(FileConfigTestCase):
def test_prioritized_files(self):
@ -135,7 +32,6 @@ require = %(here)s/first.conf
self.assertEqual(len(files), 2)
self.assertEqual(files[0], second)
self.assertEqual(files[1], first)
self.assertIs(files, myconfig.get_prioritized_files())
def test_setdefault(self):
myconfig = config.RattailConfig()
@ -164,12 +60,42 @@ require = %(here)s/first.conf
# try that for get() too
self.assertRaises(ValueError, myconfig.get, 'foo', 'bar', 'blarg', 'blast')
def test_get(self):
myconfig = config.RattailConfig()
myconfig.setdefault('foo.bar', 'baz')
# can pass section + option
self.assertEqual(myconfig.get('foo', 'bar'), 'baz')
# or can pass just a key
self.assertEqual(myconfig.get('foo.bar'), 'baz')
# so 1 or 2 args required, otherwise error
self.assertRaises(ValueError, myconfig.get, 'foo', 'bar', 'baz')
self.assertRaises(ValueError, myconfig.get)
def test_getbool(self):
myconfig = config.RattailConfig()
self.assertFalse(myconfig.getbool('foo.bar'))
myconfig.setdefault('foo.bar', 'true')
self.assertTrue(myconfig.getbool('foo.bar'))
def test_get_date(self):
myconfig = config.RattailConfig()
self.assertIsNone(myconfig.get_date('foo.date'))
myconfig.setdefault('foo.date', '2023-11-20')
value = myconfig.get_date('foo.date')
self.assertIsInstance(value, datetime.date)
self.assertEqual(value, datetime.date(2023, 11, 20))
def test_getdate(self):
myconfig = config.RattailConfig()
self.assertIsNone(myconfig.getdate('foo.date'))
myconfig.setdefault('foo.date', '2023-11-20')
value = myconfig.getdate('foo.date')
self.assertIsInstance(value, datetime.date)
self.assertEqual(value, datetime.date(2023, 11, 20))
def test_getint(self):
myconfig = config.RattailConfig()
self.assertIsNone(myconfig.getint('foo.bar'))
@ -182,20 +108,6 @@ require = %(here)s/first.conf
myconfig.setdefault('foo.bar', 'hello world')
self.assertEqual(myconfig.getlist('foo.bar'), ['hello', 'world'])
def test_getdate(self):
myconfig = config.RattailConfig()
self.assertIsNone(myconfig.getdate('foo.date'))
myconfig.setdefault('foo.date', '2023-11-20')
value = myconfig.getdate('foo.date')
self.assertIsInstance(value, datetime.date)
self.assertEqual(value, datetime.date(2023, 11, 20))
def test_get_app(self):
myconfig = config.RattailConfig()
app = myconfig.get_app()
self.assertIsInstance(app, AppHandler)
self.assertIs(type(app), AppHandler)
def test_parse_bool(self):
myconfig = config.RattailConfig()
self.assertTrue(myconfig.parse_bool('true'))
@ -218,11 +130,195 @@ require = %(here)s/first.conf
value = myconfig.make_list_string(["you don't", 'say'])
self.assertEqual(value, "\"you don't\", say")
def test_get_app(self):
myconfig = config.RattailConfig()
app = myconfig.get_app()
self.assertIsInstance(app, AppHandler)
self.assertIs(type(app), AppHandler)
def test_beaker_invalidate_setting(self):
# TODO: this doesn't really test anything, just gives coverage
myconfig = config.RattailConfig()
myconfig.beaker_invalidate_setting('foo')
def test_production(self):
myconfig = config.RattailConfig()
# false if not defined
self.assertFalse(myconfig.production())
# but config may specify
myconfig.setdefault('rattail.production', 'true')
self.assertTrue(myconfig.production())
def test_node_type(self):
myconfig = config.RattailConfig()
# error if node type not defined
self.assertRaises(ConfigurationError, myconfig.node_type)
# unless default is provided
self.assertEqual(myconfig.node_type(default='foo'), 'foo')
# or config contains the definition
myconfig.setdefault('rattail.node_type', 'bar')
self.assertEqual(myconfig.node_type(), 'bar')
def test_get_model(self):
myconfig = config.RattailConfig()
# default is rattail.db.model
model = myconfig.get_model()
self.assertIs(model, sys.modules['rattail.db.model'])
# or config may specify
myconfig.setdefault('rattail.model', 'rattail.trainwreck.db.model')
model = myconfig.get_model()
self.assertIs(model, sys.modules['rattail.trainwreck.db.model'])
def test_get_enum(self):
myconfig = config.RattailConfig()
# default is rattail.enum
enum = myconfig.get_enum()
self.assertIs(enum, sys.modules['rattail.enum'])
# or config may specify
# (nb. using bogus example module here)
myconfig.setdefault('rattail.enum', 'rattail.auth')
enum = myconfig.get_enum()
self.assertIs(enum, sys.modules['rattail.auth'])
def test_get_trainwreck_model(self):
myconfig = config.RattailConfig()
# error if not defined
self.assertRaises(ConfigurationError, myconfig.get_trainwreck_model)
# but config may specify
myconfig.setdefault('rattail.trainwreck.model', 'rattail.trainwreck.db.model')
model = myconfig.get_trainwreck_model()
self.assertIs(model, sys.modules['rattail.trainwreck.db.model'])
def test_versioning_enabled(self):
myconfig = config.RattailConfig()
# false by default
self.assertFalse(myconfig.versioning_enabled())
# but config may enable
myconfig.setdefault('rattail.db.versioning.enabled', 'true')
self.assertTrue(myconfig.versioning_enabled())
def test_app_package(self):
myconfig = config.RattailConfig()
# error if not defined
self.assertRaises(ConfigurationError, myconfig.app_package)
# unless default is provided
self.assertEqual(myconfig.app_package(default='foo'), 'foo')
# but config may specify
myconfig.setdefault('rattail.app_package', 'bar')
self.assertEqual(myconfig.app_package(), 'bar')
def test_app_title(self):
myconfig = config.RattailConfig()
# default title
self.assertEqual(myconfig.app_title(), 'Rattail')
# but config may specify
myconfig.setdefault('rattail.app_title', 'Foo')
self.assertEqual(myconfig.app_title(), 'Foo')
def test_node_title(self):
myconfig = config.RattailConfig()
# default title
self.assertEqual(myconfig.app_title(), 'Rattail')
# but config may specify
myconfig.setdefault('rattail.node_title', 'Foo (node)')
self.assertEqual(myconfig.node_title(), 'Foo (node)')
def test_running_from_source(self):
myconfig = config.RattailConfig()
# false by default
self.assertFalse(myconfig.running_from_source())
# but config may enable
myconfig.setdefault('rattail.running_from_source', 'true')
self.assertTrue(myconfig.running_from_source())
def test_demo(self):
myconfig = config.RattailConfig()
# false by default
self.assertFalse(myconfig.demo())
# but config may enable
myconfig.setdefault('rattail.demo', 'true')
self.assertTrue(myconfig.demo())
def test_appdir(self):
myconfig = config.RattailConfig()
# can be none if required is false
self.assertIsNone(myconfig.appdir(require=False))
# otherwise sane fallback is used
with patch('rattail.config.sys') as sys:
sys.prefix = 'foo'
path = os.path.join('foo', 'app')
self.assertEqual(myconfig.appdir(), path)
# or config may specify
myconfig.setdefault('rattail.appdir', '/foo/bar/baz')
self.assertEqual(myconfig.appdir(), '/foo/bar/baz')
def test_datadir(self):
myconfig = config.RattailConfig()
# error if not defined
self.assertRaises(ConfigurationError, myconfig.datadir)
# but can avoid error if not required
self.assertIsNone(myconfig.datadir(require=False))
# or config may specify
myconfig.setdefault('rattail.datadir', '/foo/bar/baz')
self.assertEqual(myconfig.datadir(), '/foo/bar/baz')
def test_workdir(self):
myconfig = config.RattailConfig()
# error if not defined
self.assertRaises(ConfigurationError, myconfig.workdir)
# but can avoid error if not required
self.assertIsNone(myconfig.workdir(require=False))
# or config may specify
myconfig.setdefault('rattail.workdir', '/foo/bar/baz')
self.assertEqual(myconfig.workdir(), '/foo/bar/baz')
def test_batch_filedir(self):
myconfig = config.RattailConfig()
# error if not defined
self.assertRaises(ConfigurationError, myconfig.batch_filedir)
# config may specify
path = os.path.join(os.sep, 'foo', 'files')
myconfig.setdefault('rattail.batch.files', path)
self.assertEqual(myconfig.batch_filedir(), path)
# caller may specify a key
self.assertEqual(myconfig.batch_filedir(key='bar'), os.path.join(path, 'bar'))
class TestRattailDefaultFiles(FileConfigTestCase):