diff --git a/src/wuttjamaican/conf.py b/src/wuttjamaican/conf.py index b207681..daad029 100644 --- a/src/wuttjamaican/conf.py +++ b/src/wuttjamaican/conf.py @@ -1013,3 +1013,89 @@ def make_config( extension.startup(config) return config + + +class WuttaConfigProfile: + """ + Base class to represent a configured "profile" in the context of + some service etc. + + :param config: App :term:`config object`. + + :param key: Config key for the profile. + + Generally each subclass will represent a certain type of config + profile, and each instance will represent a single profile + (identified by the ``key``). + """ + + def __init__(self, config, key): + self.config = config + self.app = self.config.get_app() + self.key = key + self.load() + + @property + def section(self): + """ + The primary config section under which profiles may be + defined. + + There is no default; each subclass must declare it. + + This corresponds to the typical INI file section, for instance + a section of ``wutta.telemetry`` assumes file contents like: + + .. code-block:: ini + + [wutta.telemetry] + default.submit_url = /nodes/telemetry + special.submit_url = /nodes/telemetry-special + """ + raise NotImplementedError + + def load(self): + """ + Read all relevant settings from config, and assign attributes + on the profile instance accordingly. + + There is no default logic but subclass will generally override. + + While a caller can use :meth:`get_str()` to obtain arbitrary + config values dynamically, it is often useful for the profile + to pre-load some config values. This allows "smarter" + interpretation of config values in some cases, and at least + ensures common/shared logic. + + There is no constraint or other guidance in terms of which + profile attributes might be set by this method. Subclass + should document if necessary. + """ + + def get_str(self, option, **kwargs): + """ + Get a string value for the profile, from config. + + :param option: Name of config option for which to return value. + + This just calls :meth:`~WuttaConfig.get()` on the config + object, but for a particular setting name which it composes + dynamically. + + Assuming a config file like: + + .. code-block:: ini + + [wutta.telemetry] + default.submit_url = /nodes/telemetry + + Then a ``default`` profile under the ``wutta.telemetry`` + section would effectively have a ``submit_url`` option:: + + class TelemetryProfile(WuttaConfigProfile): + section = "wutta.telemetry" + + profile = TelemetryProfile("default") + url = profile.get_str("submit_url") + """ + return self.config.get(f'{self.section}.{self.key}.{option}', **kwargs) diff --git a/tests/test_conf.py b/tests/test_conf.py index 5b46539..75271ca 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -12,7 +12,7 @@ from wuttjamaican import conf as mod from wuttjamaican import conf from wuttjamaican.exc import ConfigurationError from wuttjamaican.app import AppHandler -from wuttjamaican.testing import FileTestCase +from wuttjamaican.testing import FileTestCase, ConfigTestCase class TestWuttaConfig(FileTestCase): @@ -867,3 +867,21 @@ class TestMakeConfig(FileTestCase): foo_cls.assert_called_once_with() foo_obj.configure.assert_called_once_with(testconfig) foo_obj.startup.assert_called_once_with(testconfig) + + +class TestWuttaConfigProfile(ConfigTestCase): + + def make_profile(self, key): + return mod.WuttaConfigProfile(self.config, key) + + def test_section(self): + profile = self.make_profile('default') + self.assertRaises(NotImplementedError, getattr, profile, 'section') + + def test_get_str(self): + self.config.setdefault('wutta.telemetry.default.submit_url', '/nodes/telemetry') + with patch.object(mod.WuttaConfigProfile, 'section', new='wutta.telemetry'): + profile = self.make_profile('default') + self.assertEqual(profile.section, 'wutta.telemetry') + url = profile.get_str('submit_url') + self.assertEqual(url, '/nodes/telemetry')