User Guide

Testing

Testing with a ConfigSingleton

Because a ConfigSingleton is shared across your application, tests that modify it can affect each other. Use _reset() in tearDown to restore the singleton to a clean state after each test.

from unittest import TestCase
from yourmodule.conf import CFG

class MyTests(TestCase):
    def tearDown(t):
        CFG._reset()

    def test_with_override(t):
        from batconf import insert_source, NamespaceSource
        from argparse import Namespace
        args = Namespace()
        setattr(args, 'yourmodule.server.host', 'testhost')
        insert_source(cfg=CFG, source=NamespaceSource(args))
        t.assertEqual(CFG.server.host, 'testhost')

    def test_reads_default(t):
        # CFG was reset by tearDown — reads from the real sources again
        t.assertEqual(CFG.server.host, 'localhost')

Testing without a singleton

If test isolation is a concern, call get_config() directly in each test instead of using the shared CFG. This creates a fresh Configuration per test with no shared state.

from yourmodule.conf import get_config

class MyTests(TestCase):
    def test_something(t):
        cfg = get_config()
        t.assertEqual(cfg.server.host, 'localhost')

Custom Configuration Sources

Any object with a get(key, path) method satisfies SourceInterfaceP, so you can pull config values from any backend — a secrets manager, a database, a remote API — without changing the rest of your config setup.

Using the Protocol (structural subtyping)

The simplest approach: implement get on any class, no base class required.

class VaultSource:
    def __init__(self, client):
        self._client = client

    def get(self, key: str, path: str | None = None) -> str | None:
        full_key = f'{path}.{key}' if path else key
        return self._client.read(full_key)

Subclassing SourceInterface

Subclassing SourceInterface gives you the ABC enforcement and is useful if you want type checkers to flag incomplete implementations.

from batconf.source import SourceInterface

class VaultSource(SourceInterface):
    def __init__(self, client):
        self._client = client

    def get(self, key: str, path: str | None = None) -> str | None:
        full_key = f'{path}.{key}' if path else key
        return self._client.read(full_key)

Registering a custom source

Pass your custom source to SourceList like any built-in source, or add it at runtime with insert_source():

from batconf import insert_source
from yourmodule.conf import CFG

insert_source(cfg=CFG, source=VaultSource(vault_client))

The source is inserted at index 0 by default, giving it the highest priority. Pass index= to place it elsewhere in the lookup order.

Important constraints

  • get must return a str or None — never a non-string value. Some sources (e.g. environment variables) can only store strings, and BatConf treats any falsey return value (False, None, "") as “not found”.

  • Keep get fast and side-effect-free — it is called on every config lookup, not cached by BatConf itself.