Source code for surfactant.configmanager
import os
import platform
from pathlib import Path
from threading import Lock
from typing import Any, Dict, Optional, Union
import tomlkit
[docs]
class ConfigManager:
"""A configuration manager for handling settings stored in a configuration file. The
configuration manager internally caches a copy of the loaded configuration file, so
external changes won't affect the setting value while a program is running.
Attributes:
app_name (str): The name of the application. (Default: 'surfactant')
config_dir (Optional[Path]): The directory where the configuration file is stored.
config (tomlkit.document): The configuration document loaded by tomlkit. Preserves formatting and comments.
config_file_path (Path): The path to the configuration file.
"""
_initialized: bool = False
_instances: Dict[str, "ConfigManager"] = {}
_lock = Lock()
def __new__(
cls, app_name: str = "surfactant", config_dir: Optional[Union[str, Path]] = None
) -> "ConfigManager":
"""Manage singleton configuration manager for each unique application name.
Args:
app_name (str): The name of the application. (Default: 'surfactant')
config_dir (Optional[Union[str, Path]]): The directory where the application configuration is stored.
Returns:
ConfigManager: The singleton instance of the configuration manager for the given application name.
"""
with cls._lock:
if app_name not in cls._instances:
instance = super(ConfigManager, cls).__new__(cls)
instance._initialized = False
cls._instances[app_name] = instance
return cls._instances[app_name]
def __init__(
self, app_name: str = "surfactant", config_dir: Optional[Union[str, Path]] = None
) -> None:
"""Initializes the configuration manager.
Args:
app_name (str): The name of the application. (Default: 'surfactant')
config_dir (Optional[Union[str, Path]]): The directory where the application configuration is stored.
"""
if self._initialized:
return
self._initialized = True
self.app_name = app_name
self.config_dir = Path(config_dir) / app_name if config_dir else None
self.config = tomlkit.document()
self.config_file_path = self._get_config_file_path()
self._load_config()
def _get_config_file_path(self) -> Path:
"""Determines the path to the configuration file.
Returns:
Path: The path to the configuration file.
"""
if self.config_dir:
config_dir = Path(self.config_dir)
else:
if platform.system() == "Windows":
config_dir = Path(os.getenv("APPDATA", str(Path("~\\AppData\\Roaming"))))
else:
config_dir = Path(os.getenv("XDG_CONFIG_HOME", str(Path("~/.config"))))
config_dir = config_dir / self.app_name / "config.toml"
return config_dir.expanduser()
def _load_config(self) -> None:
"""Loads the configuration from the configuration file."""
if self.config_file_path.exists():
with open(self.config_file_path, "r") as configfile:
self.config = tomlkit.parse(configfile.read())
[docs]
def get(self, section: str, option: str, fallback: Optional[Any] = None) -> Any:
"""Gets a configuration value.
Args:
section (str): The section within the configuration file.
option (str): The option within the section.
fallback (Optional[Any]): The fallback value if the option is not found.
Returns:
Any: The configuration value or the fallback value.
"""
return self.config.get(section, {}).get(option, fallback)
[docs]
def set(self, section: str, option: str, value: Any) -> None:
"""Sets a configuration value.
Args:
section (str): The section within the configuration file.
option (str): The option within the section.
value (Any): The value to set.
"""
if section not in self.config:
self.config[section] = tomlkit.table()
self.config[section][option] = value
self._save_config()
def _save_config(self) -> None:
"""Saves the configuration to the configuration file."""
if not self.config_file_path.exists():
self.config_file_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_file_path, "w") as configfile:
configfile.write(tomlkit.dumps(self.config))
def __getitem__(self, key: str) -> Any:
"""Enables dictionary-like syntax for accessing configuration settings.
NOTE: Remember to check that the value returned is not 'None' before
trying to access nested keys.
Args:
key (str): The key for accessing a TOML value or table.
Returns:
Any: The configuration value or 'NoneType' if the key doesn't exist.
"""
if key not in self.config:
return None
return self.config[key]
[docs]
@classmethod
def delete_instance(cls, app_name: str) -> None:
"""Deletes the singleton instance for the given application name.
Args:
app_name (str): The name of the application.
"""
with cls._lock:
if app_name in cls._instances:
del cls._instances[app_name]
[docs]
def get_data_dir_path(self) -> Path:
"""Determines the path to the data directory, for storing things such as databases.
Returns:
Path: The path to the data directory.
"""
if platform.system() == "Windows":
data_dir = Path(os.getenv("LOCALAPPDATA", str(Path("~\\AppData\\Local"))))
else:
data_dir = Path(os.getenv("XDG_DATA_HOME", str(Path("~/.local/share"))))
data_dir = data_dir / self.app_name
return data_dir.expanduser()