LoginSignup
0
1

Powerful ConfigDict

Last updated at Posted at 2024-05-31

Powerful ConfigDict

Here I introduce ConfigDict which implements dictionary-like structure.
Unlike regular python dictionaries it allows to access values with dot notations.
Values can be retrieved either with get method, through dot notataion like myconfigdict.key1.key2.key3, or through square brackets like myconfigdict[key1][key2][key3].

from typing import Dict, Any, Union, Optional, Iterable

from utils import NominalToken

KeyType = Union[str, NominalToken]


class ConfigDict:
    """
    ConfigDict is a dictionary-like data structure to set and store multiple level configuration.
    The dictionary itself can be easily serialized into JSON or string format.
    """

    __slots__ = '__dict__'

    def __init__(self, preset: Optional[Dict[KeyType, Any]] = None):
        """
        constructor.

        :param preset: Optional dictionary to load config from
        """
        if isinstance(preset, ConfigDict):
            preset = preset.flatten()
        if preset is not None:
            for k, v in preset.items():
                self[k] = v

    def flatten(self) -> Dict[KeyType, Any]:
        """
        Create the flat version of itself.

        :return: A single level dictionary that maps dot-connected config path to their values.
        Empty sub-dictionary will be removed
        """
        result: Dict[KeyType, Any] = {}
        for k, v in [*self.__dict__.items()]:
            if isinstance(v, ConfigDict):
                sub = v.flatten()
                if not len(sub):
                    del self.__dict__[k]
                else:
                    for sub_k, sub_v in sub.items():
                        result[f'{k}.{sub_k}'] = sub_v
            else:
                result[k] = v
        return result

    def get(self, key: KeyType, default: Any = None) -> Any:
        """
        Try to get value for a specific config path and return default value if not found or the value is a sub dictionary

        :param key: Dot-connected config path
        :param default: Optional default value, by default is None
        :return: Configuration value
        """
        item = self[key]
        if isinstance(item, ConfigDict):
            return default
        return item

    def update(self, another: 'Union[ConfigDict, Dict[KeyType, Any]]') -> None:
        """
        Update configuration from another ConfigDict or a dictionary

        :param another: Another ConfigDict or a dictionary that maps string to values
        """
        override = another
        if isinstance(another, ConfigDict):
            override = another.flatten()
        for k, v in override.items():
            self[k] = v

    def __getitem__(self, key: KeyType):
        """
        Get the value assigned to the key

        :param key: Key
        :return: Value
        """
        if isinstance(key, str) and '.' in key:
            p = key.split('.')
            if p[0] not in self.__dict__:
                self.__dict__[p[0]] = ConfigDict()
            return self.__dict__[p[0]]['.'.join(p[1:])]
        if key not in self.__dict__:
            self.__dict__[key] = ConfigDict()
        return self.__dict__[key]

    def __setitem__(self, key: KeyType, value):
        """
        Set the value to the key

        :param key: Key
        :param value: Value
        """
        if isinstance(key, str) and '.' in key:
            p = key.split('.')
            if p[0] not in self.__dict__:
                self.__dict__[p[0]] = ConfigDict()
            self.__dict__[p[0]]['.'.join(p[1:])] = value
            return
        self.__dict__[key] = value

    def __delitem__(self, key: KeyType):
        """
        Delete key

        :param key: Key to delete
        """
        if key in self.__dict__:
            del self.__dict__[key]
            return

        if isinstance(key, str) and '.' in key:
            p = key.split('.')
            if p[0] in self.__dict__:
                del self.__dict__[p[0]]['.'.join(p[1:])]

    def __getattr__(self, key: str):
        """
        Get the value assigned to the key

        :param key: Key
        :return: Value
        """
        return self[key]

    def __setattr__(self, key: str, value):
        """
        Set the value to the key

        :param key: Key
        :param value: Value
        """
        self[key] = value

    def __delattr__(self, key: str):
        """
        Delete key

        :param key: Key to delete
        """
        del self[key]

    def __repr__(self):
        """
        String representation

        :return: String representation
        """
        return self.flatten().__repr__()

    def __len__(self):
        """
        override __len__

        :return: Numbers of keys stored
        """
        return len(self.flatten())

    def __contains__(self, key: KeyType):
        """
        override in operator.

        :param key: Key to check
        :return: True if the key exists
        """
        return key in self.__dict__ or key in self.flatten()

    def __dir__(self) -> Iterable[str]:
        """
        override dir()

        :return: Key to existing elements and methods
        """
        yield from ('get', 'flatten', 'update')
        self.flatten()
        for key in self.__dict__:
            yield str(key)

NominalToken is just a class that represents constant string values like MYCONSTANTVAL. Below is its code

class NominalToken:
    """
    Nominal token class to represent constant values and can be used as dictionary keys.
    Tokens with the same name are identical in equality comparison.
    """

    def __init__(self, name: str) -> None:
        """
        constructor.

        :param name: Token name
        """
        self._name = name

    @property
    def name(self) -> str:
        """
        Token name

        :return: Token name
        """
        return self._name

    def __eq__(self, other: Any):
        """
        Override '==' and '!=' operator

        :param other: Value to compare with
        :return: True if the other value is a NominalToken with the same name, else False
        """
        if isinstance(other, NominalToken):
            return self._name == other.name
        return False

    def __hash__(self):
        """
        Override hash().
        NominalToken with the same name will have the same hash value

        :return: Hashcode
        """
        return hash('NominalToken' + self.name)

    def __repr__(self):
        """
        String representation

        :return: String representation
        """
        return f'[{self.name}]'

You could use ConfigDict in below manners

config = ConfigDict({
    'database': {
        'host': 'localhost',
        'port': 5432
    },
    'logging': {
        'level': 'DEBUG',
        'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    }
})

# Accessing nested configuration
host = config.get('database.host')
logging_level = config.get('logging.level')

# Updating configuration
config.update({
    'database': {
        'port': 3306
    }
})

# Flatten the configuration
flat_config = config.flatten()

Happy Coding!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1