"""
This module defines the ConfigInterface class, which handles environment-specific configurations
and provides additional methods and attributes that are not directly related to configuration values.
Classes:
ConfigInterface: Manages configuration attributes and environment settings.
"""
import pickle
from fastcfg.config.base import AbstractConfigUnit
from fastcfg.config.events import EventListenerMixin
from fastcfg.config.items import AbstractConfigItem
from fastcfg.config.utils import deep_merge_config, potentially_has_children
from fastcfg.validation.validatable import ValidatableMixin
[docs]
class ConfigInterface(
ValidatableMixin, EventListenerMixin, AbstractConfigUnit
):
"""
Handles environment-specific configurations and provides additional public methods and attributes
that are not directly related to configuration values, nor are meant to be overriden by config attributes.
This separation allows the Config class to have public functions and variables without
cluttering the main configuration logic.
Attributes:
_config_attributes (ConfigAttributes): The configuration attributes.
_current_env (str): The current environment.
Methods:
__init__(config_attributes, **kwargs): Initializes the `ConfigInterface` object.
set_environment(env): Sets the current environment.
get_environment(): Retrieves the current environment.
to_dict(): Returns the configuration attributes as a dictionary.
value: Property that gets the configuration attributes as a dictionary.
"""
[docs]
def __init__(self, config, config_attributes, **kwargs):
"""
Initializes the ConfigInterface.
Args:
config_attributes: The configuration attributes.
**kwargs: Additional keyword arguments.
"""
super().__init__()
self._config = config
self._parent = None
self._config_attributes = config_attributes
self._current_env = None
[docs]
def set_parent(self, parent: "Config"):
"""Set the parent Config object."""
self._parent = parent
[docs]
def update(self, other=None, **kwargs) -> "ConfigInterface":
"""
Updates the configuration attributes using deep merge. Works like dict.update() but preserves nested values.
Can accept either a dict-like object or keyword arguments.
If an environment is currently active, updates will only affect that environment.
Dictionaries are automatically converted to Config objects.
Args:
other: A dict-like object or iterable of key-value pairs.
**kwargs: Additional keyword arguments.
"""
# Determine the target for updates
if self._current_env:
target = getattr(self._config, self._current_env)
else:
target = self._config
# Process the main data
if other is not None:
if hasattr(other, "items"):
# Handle dict-like objects
deep_merge_config(target, other)
else:
# Handle sequence of pairs
deep_merge_config(target, dict(other))
# Handle keyword arguments
if kwargs:
deep_merge_config(target, kwargs)
# Allows for method chaining
return self
[docs]
def save(self, file_path: str):
"""
Saves the full configuration state to a file.
"""
with open(file_path, "wb") as f:
pickle.dump(self._config, f)
[docs]
def load(self, file_path: str):
"""
Loads the full configuration state from a file.
"""
with open(file_path, "rb") as f:
self._config = pickle.load(f)
[docs]
def export_values(self, file_path: str):
"""
Exports the current configuration values to a file.
"""
from fastcfg.util import save_file
save_file(file_path, self._config.to_dict())
[docs]
def import_values(self, file_path: str):
"""
Imports the current configuration values from a file using deep merge.
If an environment is currently active, the import will only affect that environment.
If no environment is active, the import will affect the root configuration.
Existing nested values are preserved unless explicitly overwritten.
"""
from fastcfg.util import load_file
data = load_file(file_path)
self.update(data)
[docs]
def set_environment(self, env: str | None) -> "ConfigInterface":
"""
Sets the current environment.
Args:
env: The environment to set.
Raises:
ValueError: If the environment is invalid.
"""
if self._config_attributes.has_attribute(env) or env is None:
self._current_env = env
else:
raise ValueError(f"Invalid environment: {env}")
self._current_env = env
# Allows for method chaining
return self
[docs]
def remove_environment(self) -> "ConfigInterface":
"""
Removes the current environment.
"""
self.set_environment(None)
# Allows for method chaining
return self
@property
def environment(self) -> str | None:
"""
Gets the current environment.
Returns:
The current environment.
"""
return self._current_env
def _get_env_dict(self, only_has_children=False) -> dict:
all_attrs = self._config_attributes.get_attributes()
envs = {}
for attr, val in all_attrs.items():
if only_has_children:
# Config or dict is a valid environment
if potentially_has_children(val):
envs[attr] = val
else:
envs[attr] = val
return envs
@property
def environments(self):
"""
Gets the environments.
Returns:
The environments.
"""
from fastcfg.config.cfg import Config
envs = self._get_env_dict(only_has_children=True)
return Config(**envs)
[docs]
def to_dict(self) -> dict:
"""
Gets the configuration attributes as a dictionary. Fully serializable.
It will also resolve nested Config objects to their dictionary
representation and ConfigItems as their value.
Returns:
dict: The configuration attributes.
"""
from fastcfg.config.cfg import Config
attrs = self._get_env_dict()
# Recursively resolve nested Config objects and ConfigItems to their dictionary
# representation and their value.
for k, v in attrs.items():
if isinstance(v, Config):
attrs[k] = v.to_dict()
elif isinstance(v, AbstractConfigItem):
attrs[k] = v.value
if self._current_env:
attrs = attrs[self._current_env]
if not isinstance(attrs, dict):
attrs = attrs.__dict__
return attrs
@property
def value(self):
"""
Gets the configuration attributes as a dictionary.
Primarily used to get the value for validation.
Returns:
dict: The configuration attributes.
"""
return self.to_dict()
[docs]
def keys(self):
"""
Returns a view of the configuration's keys.
"""
return self.to_dict().keys()
[docs]
def values(self):
"""
Returns a view of the configuration's values.
"""
return self.to_dict().values()
[docs]
def items(self):
"""
Returns a view of the configuration's (key, value) pairs.
"""
return self.to_dict().items()
[docs]
def get(self, key, default=None):
"""
Get a configuration value with a default if the key doesn't exist.
"""
try:
return getattr(self._config, key)
except AttributeError:
return default
[docs]
def pop(self, key, *args):
"""
Remove and return a configuration value.
"""
if len(args) > 1:
raise TypeError(
f"pop expected at most 2 arguments, got {len(args) + 1}"
)
try:
value = getattr(self._config, key)
delattr(self._config, key)
return value
except AttributeError:
if args:
return args[0]
raise KeyError(key)