Minimalistic approach
When my application requires a centralized place for a config, I usually start with a dedicated file holding a dict()
object:
# file: config.py
config = dict()
The usage across the application is really simple - import and use it:
# file: app.py
from config import config
config['db'] = 'uat01'
config.update(host='localhost', username='user',
password='secret pass')
Such config
has a nice property - it is a singleton without any implementation tricks.
However, I don't find it particularly handy to use dict
syntax of accessing elements with []
. Quick fix would be to either:
- 1. Create aliases for
__*attr__
methods:
# file: config.py
class Config(dict):
__getattr__ = dict.get
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
config = Config()
- 2. Use
SimpleNamespace
fromtypes
however, it lacksupdate()
equivalent method.
When things are changing
Let's stick to the first choice and have a look at the following scenario:
# file: app.py
from config import config
from types import Optional
def fibonacci(
count: int,
first: Optional[int] = None,
second: Optional[int] = None
) -> int:
a, b = first or config.first, second or config.second
for _ in range(count):
a, b = b, a + b
return a
# file: main.py
from app import fibonacci
from config import config
if __name__ == '__main__':
config.update(first=1, second=1)
print(f'{fibonacci(6)=}')
config.update(first=3, second=5)
print(f'{fibonacci(6)=}')
The result won't change between two calls to our fibonacci
function.
fibonacci(6)=8
fibonacci(6)=8
Make it aware of the changes
# file: config.py
import importlib
import inspect
class Config:
_data = dict()
_referrers = set()
def __getattr__(self, name):
caller = inspect.currentframe().f_back
module_name = caller.f_globals['__name__']
if '__main__' not in module_name:
self._referrers.add(module_name)
try:
return self._data[name]
except KeyError:
raise AttributeError('no attr %s', name)
def __setattr__(self, name, value):
self._data[name] = value
def _reload_modules(self):
for module_name in self._referrers.copy():
module = importlib.import_module(module_name)
importlib.reload(module)
def __repr__(self):
return 'Config(%r)' % self._data
def update(self, reload=True, **kwargs):
for k, v in kwargs.items()
self.__setattr__(k, v)
if reload:
self._reload_modules()
config = Config()
# set defaults
config.update(first=0, second=1)
Now, running main.py
yields the following result:
fibonacci(6)=8
fibonacci(6)=55
What's going on?
Ok, the newly defined Config
class is a bit lengthy and requires some explanation.
Let's go through the code:
import importlib
import inspect
These two imports will come in handy in a minute, bare with me.
class Config:
_data = dict()
_referrers = set()
The Config
class encapsulates two elements: 1. _data
, the actual key-value container and 2. _referrers
, the set of other modules that import config
and access its entries.
def __getattr__(self, name):
caller = inspect.currentframe().f_back
module_name = caller.f_globals['__name__']
if '__main__' not in module_name:
self._referrers.add(module_name)
Custom __getattr__
enables dot access syntax. Additionally, it is the moment in which I record modules that refer to config
. When config
is updated, it will allow me to let those modules know that something has changed and it is time to reload.
The rest is pretty straightforward. In:
def _reload_modules(self):
for module_name in self._referrers.copy():
module = importlib.import_module(module_name)
importlib.reload(module)
I loop over related modules and reload them.
Finally, at the end of the file, I have to set default values for entries that are used in other parts of the code. The other option would be to load a config file and parse it.
Also, by design I decided that only call to update
refresh referred modules. Setting them one by one with __setattr__
does not have this behavior.
What is your approach to config?
Top comments (0)