Skip to main content
1 of 2
riskypenguin
  • 3.5k
  • 1
  • 9
  • 28

Singleton handler for reading and writing to configuration.json

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:

  1. General approach
  2. Comparison between the haskell-like approach of get_setting vs. the simpler and more readable approach of get_setting_old
  3. 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 to json. Maybe I'm missing something here.
  4. 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.
riskypenguin
  • 3.5k
  • 1
  • 9
  • 28