In a current project of I have multiple modules that need to read from and write to a config.json
file, where the settings are saved as nested dictionaries. I chose json
here, because I want the config file to be human-readable.
Simplified example config.json:
{
"server_data": {
"email_address": "[email protected]",
"server_address": "imap.server.com",
"server_port": 993
},
"handle_emails": {
"output_directory": "C:\\Users\\riskypenguin\\some_directory",
"mark_read": false,
"write_to_file": true,
"folders": ["inbox"]
},
"output": {
"output_directory": "C:\\Users\\riskypenguin\\some_other_directory",
"print_timeinfo": true,
"print_form_counts": true
}
}
My config_handler.py
provides standardised access to the file via get_setting
and set_setting
. For different use cases throughout the project I need to be able to access the configuration settings at varying depths. For example: Some boolean
settings are read and written directly, but the dict
at server_data
will be read and processed as a whole. Concurrency is not relevant for this project, so there are no collisions regarding read and write access.
config_handler.py
import os
import json
from functools import reduce
from definitions import CONFIG_DIR, CONFIG_FILE, DEFAULT_CONFIG_FILE
__all__ = ["get_setting", "set_setting"]
# Helper functions
def _save_config_data(content):
if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
with open(CONFIG_FILE, 'w') as f:
json.dump(obj=content, fp=f, indent=2)
def _load_config_data():
file = CONFIG_FILE if os.path.exists(CONFIG_FILE) else DEFAULT_CONFIG_FILE
with open(file, 'r') as f:
return json.load(f)
_config_data = _load_config_data()
# Exported functions
# Old version of get_setting, currently not in use
def get_setting_old(*args, default=None):
current_value = _config_data
for key in args:
if isinstance(current_value, dict):
current_value = current_value.get(key, default)
else:
return default
return current_value
def get_setting(*config_keys, default=None):
drill_down = lambda x, y: dict.get(x, y, default) if isinstance(x, dict) else default
return reduce(drill_down, config_keys, _config_data)
def set_setting(*config_keys, value):
current_value = _config_data
for key in config_keys[:-1]:
if isinstance(current_value, dict):
current_value = current_value.get(key)
else:
return False
last_key = config_keys[-1]
if isinstance(current_value, dict):
current_value[last_key] = value
else:
return False
_save_config_data(_config_data)
return True
As this is my first shot at handling configurations in a central way like this I am grateful for all feedback. I'm particularly interested in feedback about:
- General approach
- Comparison between the haskell-like approach of
get_setting
vs. the simpler and more readable approach ofget_setting_old
set_setting
seems rather unelegant to me. As the types of a setting can be either mutable or immutable I don't see a better solution though. I've thought about wrapper classes for primitive types, but that feels rather unpythonic and creates some unnecessary overhead when reading from and before writing tojson
. Maybe I'm missing something here.- I've been thinking about creating a simple class for a path of config_keys, to be able to pass a ConfigKeyPath object instead of a bunch of separated string arguments. I'm not sure which of the two approaches is better.
__getitem__
not allowed? \$\endgroup\$get_setting
it to use__getitem__
instead ofdict.get
should be easy enough though. \$\endgroup\$