Loaders
The Configr library provides a flexible loading system for different configuration file formats. This page documents the loaders included with Configr and how to create custom loaders for additional file formats.
Overview
Loaders in Configr:
- Read configuration files in specific formats (JSON, YAML, etc.)
- Parse those files into Python dictionaries
- Allow the
ConfigBaseclass to convert the dictionaries into typed dataclasses
Base Classes
ConfigLoader
All loaders in Configr inherit from the abstract base class ConfigLoader.
from abc import ABC, abstractmethod
from typing import Any, TypeVar
T = TypeVar('T')
class ConfigLoader(ABC):
"""
Abstract base class for configuration loaders.
Defines the common interface that all configuration loaders must implement.
Configuration loaders are responsible for loading configuration data from
various sources and returning it as a dictionary.
"""
@classmethod
@abstractmethod
def load(cls, name: str, config_class: type[T]) -> dict[str, Any]:
"""
Load configuration data for the specified configuration class.
Args:
name (str): The name or identifier of the configuration to load.
config_class (type[T]): The dataclass type for which to load configuration.
Returns:
dict[str, Any]: The loaded configuration data as a dictionary.
Raises:
ConfigFileNotFoundError: If the configuration cannot be found.
TypeError: If the config_class is not a dataclass.
"""
pass
FileConfigLoader
For file-based configuration sources, Configr provides the FileConfigLoader abstract class:
class FileConfigLoader(ConfigLoader, ABC):
"""
Abstract base class for file-based configuration loaders.
Extends the ConfigLoader interface with file-specific functionality,
providing common methods for locating and loading configuration files.
File-based loaders support specific file extensions and search in a
configurable directory.
"""
_config_dir: ClassVar[str] = os.environ.get('CONFIG_DIR', '_config')
ext: list[str] # Register list of supported file extensions
@classmethod
def get_extensions(cls) -> list[str]:
"""Get the supported file extensions."""
return cls.ext
@classmethod
def get_config_path(cls) -> Path:
"""Get the base configuration directory path."""
return Path(cls._config_dir)
@classmethod
def set_config_dir(cls, config_dir: str | Path) -> None:
"""Set the base config directory path if default should not be used."""
if isinstance(config_dir, Path):
cls._config_dir = str(config_dir)
else:
cls._config_dir = config_dir
@classmethod
def _iter_config_file_paths(cls, name: str) -> Generator[Path, Any, None]:
"""Iterate over all possible configuration file paths."""
# Implementation details
@classmethod
def _get_config_file_path(cls, name: str) -> Path:
"""Get full path to config file (first that exists)."""
# Implementation details
@classmethod
def config_file_exists(cls, name: str) -> bool:
"""Check if config file exists."""
# Implementation details
Built-in Loaders
JSONConfigLoader
Loader for JSON configuration files.
class JSONConfigLoader(FileConfigLoader):
"""
Loader for JSON configuration files.
This class provides functionality to load configuration data from JSON files
and map it to the fields of a dataclass.
"""
ext: list[str] = ['.json'] # Supported file extensions for JSON configuration files.
@classmethod
def load(cls, name: str, config_class: type[T] = None) -> dict[str, Any]:
"""
Load JSON configuration from the specified path.
Args:
name (str): The name of the configuration file (without extension).
config_class (type[T]): The dataclass type to map the configuration data to.
Returns:
dict[str, Any]: A dictionary containing the loaded configuration data.
Raises:
FileNotFoundError: If the specified configuration file does not exist.
json.JSONDecodeError: If the file contains invalid JSON.
"""
# Implementation details
Example JSON configuration file:
{
"host": "localhost",
"port": 5432,
"username": "admin",
"password": "secure_password",
"database": "my_app"
}
YAMLConfigLoader
Loader for YAML configuration files. Requires PyYAML to be installed.
class YAMLConfigLoader(FileConfigLoader):
"""
Loader for YAML configuration files.
This class provides functionality to load configuration data from YAML files
and map it to the fields of a dataclass. It supports files with `.yaml` and `.yml` extensions.
"""
ext: list[str] = ['.yaml', '.yml'] # Supported file extensions for YAML configuration files.
@classmethod
def load(cls, name: str, config_class: type[T] = None) -> dict[str, Any]:
"""
Load YAML configuration from the specified path.
Args:
name (str): The name of the configuration file (without extension).
config_class (type[T]): The dataclass type to map the configuration data to.
Returns:
dict[str, Any]: A dictionary containing the loaded configuration data.
Raises:
ImportError: If PyYAML is not installed.
FileNotFoundError: If the specified configuration file does not exist.
yaml.YAMLError: If the file contains invalid YAML.
"""
# Implementation details
Example YAML configuration file:
EnvVarConfigLoader
Loader for environment variable configuration.
from configr.loaders.loader_env_var import EnvVarConfigLoader
class EnvVarConfigLoader(ConfigLoader):
"""
Loader for environment variable configuration.
This class provides functionality to load configuration data from environment
variables. Environment variables should follow the pattern:
{CONFIG_NAME}_{FIELD_NAME} where both parts are uppercase.
For nested dataclasses, the pattern extends to:
{CONFIG_NAME}_{PARENT_FIELD}_{CHILD_FIELD}
Type conversion is performed automatically for:
- Booleans: "true", "false", "1", "0"
- Integers: Numeric strings
- Floats: Decimal numeric strings
- Lists: Comma-separated values
"""
@classmethod
def load(cls, name: str, config_class: type[T]) -> dict[str, Any]:
"""
Load configuration from environment variables.
Args:
name (str): The prefix for environment variables (converted to uppercase).
config_class (type[T]): The dataclass type to map the configuration data to.
Returns:
dict[str, Any]: A dictionary containing the loaded configuration data.
"""
# Implementation details
Example usage:
from configr import config_class, ConfigBase
# When no file_name is specified, and no file with ending for
# configured loader exists, EnvVarConfigLoader is used
@config_class()
class AppConfig:
debug: bool = False
host: str = "localhost"
port: int = 8080
database_url: str = None
# Set environment variables
# export APPCONFIG_DEBUG=true
# export APPCONFIG_HOST=0.0.0.0
# export APPCONFIG_PORT=3000
# export APPCONFIG_DATABASE_URL=postgresql://localhost/mydb
# Load from environment variables
config = ConfigBase.load(AppConfig)
DotEnvConfigLoader
Loader for .env configuration files. Requires python-dotenv to be installed.
class DotEnvConfigLoader(FileConfigLoader):
"""
Loader for .env configuration files.
This class provides functionality to load configuration data from .env files
and map it to the fields of a dataclass. It leverages the environment variable
processing logic for type conversion and nested structure handling.
"""
ext: list[str] = ['.env'] # Supported file extensions for .env configuration files.
@classmethod
def load(cls, name: str, config_class: type[T]) -> dict[str, Any]:
"""
Load .env configuration from the specified path.
Args:
name (str): The prefix for environment variable names (used for variable naming).
config_class (type[T]): The dataclass type to map the configuration data to.
Returns:
dict[str, Any]: A dictionary containing the loaded configuration data.
Raises:
ImportError: If python-dotenv is not installed.
FileNotFoundError: If the .env file does not exist in the config directory.
"""
# Implementation details
@classmethod
def config_file_exists(cls, name: str) -> bool:
"""
Check if .env file exists in the config directory.
Args:
name (str): The configuration name (not used for .env files).
Returns:
bool: True if .env file exists, False otherwise.
"""
# Implementation details
@classmethod
def get_config_name(cls, config_class: type) -> str:
"""
Get config name from the dataclass name.
Args:
config_class (type): The dataclass type to extract the name from.
Returns:
str: The configuration name in uppercase, derived from the class name.
"""
# Implementation details
Example .env configuration file:
# Configuration for AppConfig class (uses APP_ prefix)
APP_DEBUG=true
APP_HOST=0.0.0.0
APP_PORT=3000
APP_DATABASE_URL=postgresql://localhost/mydb
# Nested configuration example
APP_DATABASE_HOST=localhost
APP_DATABASE_PORT=5432
APP_DATABASE_USERNAME=admin
APP_DATABASE_PASSWORD=secret
Example usage:
from configr import config_class, ConfigBase
from typing import Optional
@config_class(file_name=".env")
class AppConfig:
debug: bool = False
host: str = "localhost"
port: int = 8080
database_url: Optional[str] = None
# Load from .env file (requires python-dotenv)
config = ConfigBase.load(AppConfig)
Creating Custom Loaders
You can extend Configr with support for additional file formats by creating custom loaders.
Example: TOMLConfigLoader
Here's an example of how to create a loader for TOML files:
from typing import Any, TypeVar
from configr.loaders.loader_base import FileConfigLoader
T = TypeVar('T')
class TOMLConfigLoader(FileConfigLoader):
"""Loader for TOML configuration files."""
ext: list[str] = ['.toml'] # Supported file extensions
@classmethod
def load(cls, name: str, config_class: type[T] = None) -> dict[str, Any]:
"""Load TOML configuration from the specified path."""
try:
import toml
except ImportError:
raise ImportError("The 'toml' package is required for TOML support. Install with 'pip install toml'.")
config_file_path = cls._get_config_file_path(name)
with open(config_file_path) as f:
return toml.load(f)
# Register the loader with Configr
from configr import ConfigBase
ConfigBase.add_loader(TOMLConfigLoader)
Once registered, you can use TOML files with your configuration classes:
from configr import config_class
@config_class(file_name="database.toml")
class DatabaseConfig:
host: str
port: int
username: str
password: str
database: str
Example TOML configuration file:
Example: INIConfigLoader
Here's an example of a loader for INI files using Python's built-in configparser:
import configparser
from pathlib import Path
from typing import Any, TypeVar
from configr.loaders.loader_base import FileConfigLoader
T = TypeVar('T')
class INIConfigLoader(FileConfigLoader):
"""Loader for INI configuration files."""
ext: list[str] = ['.ini'] # Supported file extensions
@classmethod
def load(cls, name: str, config_class: type[T] = None) -> dict[str, Any]:
"""Load INI configuration from the specified path."""
config_file_path = cls._get_config_file_path(name)
config = configparser.ConfigParser(interpolation=None)
config.read(config_file_path)
return cls.as_dict(config)
@staticmethod
def as_dict(config: configparser.ConfigParser) -> dict[str, Any]:
"""
Convert the parsed INI configuration to a nested dictionary.
This method transforms the ConfigParser representation into a dictionary structure
where each section becomes a top-level key containing a dictionary of its options.
Default values are included at the top level of the resulting dictionary.
Returns:
dict[str, Any]: A nested dictionary representation of the configuration.
"""
config_dict = {}
for default_name, default_value in config.defaults().items():
config_dict[default_name] = default_value
for section in config.sections():
config_dict[section] = {
option: config.get(section, option)
for option in config.options(section)
if option not in config.defaults()
}
return config_dict
# Register the loader with Configr
from configr import ConfigBase
ConfigBase.add_loader(INIConfigLoader)
Example INI configuration file:
[DEFAULT]
application = MyApp
[database]
host = localhost
port = 5432
username = admin
password = secure_password
database = my_app
[logging]
level = INFO
file = app.log
format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
Managing Loaders
Registering Custom Loaders
Register a custom loader with ConfigBase.add_loader:
from configr import ConfigBase
from my_loaders import CustomLoader
ConfigBase.add_loader(CustomLoader)
Removing Loaders
Remove a loader with ConfigBase.remove_loader:
from configr import ConfigBase
from configr.loaders.loader_yaml import YAMLConfigLoader
ConfigBase.remove_loader(YAMLConfigLoader)
Getting Available Loaders
Get all available loaders with ConfigBase.get_available_loaders:
from configr import ConfigBase
loaders = ConfigBase.get_available_loaders()
print(loaders) # [JSONConfigLoader, YAMLConfigLoader, ...]
Getting Available File Loaders
Get all available file-based loaders with ConfigBase.get_available_file_loaders:
from configr import ConfigBase
file_loaders = ConfigBase.get_available_file_loaders()
print(file_loaders) # [JSONConfigLoader, YAMLConfigLoader, ...]
Loader Selection Process
When loading a configuration, Configr selects the appropriate loader based on the following process:
- If explicit
config_datais provided toConfigBase.load(), no loader is used. - For file-based configuration:
- If a file extension is specified in
_config_file_name, Configr looks for that exact file. - Otherwise, it tries each available file loader to find a matching file.
- If a file extension is specified in
- If no file loader can find a matching file, it tries any non-file loaders.
- If no loader can be found, a
ValueErroris raised.
Best Practices for Custom Loaders
- Error Handling: Include clear error messages for missing dependencies or file format issues.
- Dependencies: Document any external dependencies required by your loader.
- Type Conversion: Ensure your loader returns data as expected by Configr (dictionaries with the correct types).
- Configuration Directory: Use the
_config_dirpath provided byFileConfigLoaderfor file locations. - File Extension List: Define a clear list of supported file extensions in the
extclass variable.
Example Usage
from configr import config_class, ConfigBase
from my_loaders import XMLConfigLoader
# Register custom XML loader
ConfigBase.add_loader(XMLConfigLoader)
@config_class(file_name="database.xml")
class DatabaseConfig:
host: str
port: int
username: str
password: str
database: str
# Load from XML file
db_config = ConfigBase.load(DatabaseConfig)