Painless YAML configuration library for Python applications with validation, type checking, and multi-source data merging
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Support for multiple configuration data sources including YAML files, environment variables, and command-line arguments with automatic type conversion and priority handling. Configuration sources enable flexible data input from various locations with seamless merging and override capabilities.
Base classes for different types of configuration data sources that can be layered together with priority ordering.
class ConfigSource(dict):
def __init__(self, value, filename=None, default=False, base_for_paths=False):
"""
Create a configuration source from a dictionary.
Parameters:
- value (dict): Configuration data dictionary
- filename (str, optional): Source file path for this configuration data
- default (bool): Whether this source provides application defaults
- base_for_paths (bool): Use source file's directory for relative path resolution
"""
@classmethod
def of(cls, value):
"""
Create ConfigSource from dictionary or existing ConfigSource.
Parameters:
- value: Dictionary or ConfigSource object
Returns:
ConfigSource: New or existing ConfigSource object
"""Configuration sources that read from YAML files with customizable loading and error handling.
class YamlSource(ConfigSource):
def __init__(self, filename=None, default=False, base_for_paths=False,
optional=False, loader=yaml_util.Loader):
"""
Create YAML configuration source by reading from file.
Parameters:
- filename (str): Path to YAML configuration file
- default (bool): Whether this is a default configuration source
- base_for_paths (bool): Use file's directory as base for relative paths
- optional (bool): Don't raise error if file doesn't exist
- loader: PyYAML Loader class for parsing YAML
Raises:
- ConfigReadError: If file cannot be read (unless optional=True)
"""
def load(self):
"""
Load YAML data from the source's filename.
Raises:
- ConfigReadError: If file cannot be read or parsed
"""Configuration sources that read from environment variables with flexible naming and type conversion.
class EnvSource(ConfigSource):
def __init__(self, prefix, sep='__', lower=True, handle_lists=True,
parse_yaml_docs=False, loader=yaml_util.Loader):
"""
Create configuration source from environment variables.
Parameters:
- prefix (str): Environment variable prefix for identification
- sep (str): Separator within variable names for nested keys
- lower (bool): Convert variable names to lowercase after prefix matching
- handle_lists (bool): Convert sequential integer keys to lists
- parse_yaml_docs (bool): Parse values as full YAML documents vs scalars
- loader: PyYAML Loader class for parsing values
"""
def load(self):
"""
Load configuration data from environment variables.
"""
@classmethod
def _convert_dict_lists(cls, obj):
"""
Convert dicts with sequential integer keys (0, 1, 2...) to lists.
Parameters:
- obj: Dictionary potentially containing list-like structures
Returns:
Converted object with dicts converted to lists where appropriate
"""Custom YAML loading and dumping classes with enhanced features for configuration management.
class Loader(yaml.SafeLoader):
"""
Custom YAML loader with Unicode strings and OrderedDict support.
Features:
- All strings as Unicode objects
- All maps as OrderedDicts
- Strings can begin with % without quotation
"""
@staticmethod
def add_constructors(loader):
"""
Add custom constructors to a PyYAML Loader class.
Parameters:
- loader: PyYAML Loader class to modify
"""
class Dumper(yaml.SafeDumper):
"""
Custom YAML dumper with OrderedDict and formatting enhancements.
Features:
- OrderedDicts as ordinary mappings
- Short lists in inline style
- Boolean values as 'yes'/'no'
- None values as empty strings
"""
def load_yaml(filename, loader=Loader):
"""
Read YAML document from file with error handling.
Parameters:
- filename (str): Path to YAML file
- loader: PyYAML Loader class
Returns:
Parsed YAML data
Raises:
- ConfigReadError: If file cannot be read or parsed
"""
def load_yaml_string(yaml_string, name, loader=Loader):
"""
Parse YAML document from string with error handling.
Parameters:
- yaml_string (str): YAML content as string
- name (str): Name for error messages
- loader: PyYAML Loader class
Returns:
Parsed YAML data
Raises:
- ConfigReadError: If string cannot be parsed
"""
def parse_as_scalar(value, loader=Loader):
"""
Parse value as YAML scalar for consistent type conversion.
Parameters:
- value (str): String value to parse
- loader: PyYAML Loader class
Returns:
Type-converted value (int, float, bool, None, or original string)
"""
def restore_yaml_comments(data, default_data):
"""
Restore comments from default YAML to generated data.
Parameters:
- data (str): Generated YAML data string
- default_data (str): Default YAML data with comments
Returns:
str: YAML data with comments restored from default_data
"""Cross-platform utilities for configuration directory discovery and path handling.
def config_dirs():
"""
Get platform-specific configuration directory candidates.
Returns:
list: Configuration directory paths in priority order
"""
def xdg_config_dirs():
"""
Get list of paths from XDG_CONFIG_DIRS and XDG_CONFIG_HOME environment variables.
Returns:
list: XDG configuration directory paths
"""
def iter_first(sequence):
"""
Get the first element from an iterable or raise ValueError if empty.
Parameters:
- sequence: Iterable to get first element from
Returns:
First element from sequence
Raises:
ValueError: If sequence is empty
"""
def namespace_to_dict(obj):
"""
Convert argparse.Namespace or optparse.Values to dict representation.
Parameters:
- obj: Namespace or Values object to convert
Returns:
dict: Dictionary representation or original object if not convertible
"""
def find_package_path(name):
"""
Find the filesystem path for a Python package.
Parameters:
- name (str): Package name
Returns:
str or None: Package directory path, or None if not found
"""
def build_dict(obj, sep='', keep_none=False):
"""
Recursively build nested dictionary from namespace/dict with key splitting.
Parameters:
- obj: Namespace, Values, dict, or other object to convert
- sep (str): Separator for splitting keys into nested structure
- keep_none (bool): Whether to keep keys with None values
Returns:
dict: Nested dictionary structure
"""UNIX_DIR_FALLBACK = '~/.config' # Default Unix configuration directory
WINDOWS_DIR_VAR = 'APPDATA' # Windows environment variable name
WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' # Windows config directory fallback
MAC_DIR = '~/Library/Application Support' # macOS configuration directoryimport confuse
config = confuse.Configuration('myapp', read=False)
# Add specific YAML file
config.set_file('/etc/myapp/config.yaml')
# Add optional YAML file (no error if missing)
yaml_source = confuse.YamlSource('/home/user/myapp.yaml', optional=True)
config.add(yaml_source)
# YAML file with path resolution relative to file location
config_source = confuse.YamlSource('config.yaml', base_for_paths=True)
config.add(config_source)import confuse
import os
config = confuse.Configuration('myapp', read=False)
# Basic environment source with MYAPP_ prefix
config.set_env('MYAPP_')
# Environment variables:
# MYAPP_DATABASE_HOST=localhost -> config['database']['host'] = 'localhost'
# MYAPP_DATABASE_PORT=5432 -> config['database']['port'] = 5432
# Custom environment source with different settings
env_source = confuse.EnvSource(
prefix='MYAPP_',
sep='__', # Use __ as separator instead of _
lower=True, # Convert to lowercase
handle_lists=True, # Convert sequential keys to lists
parse_yaml_docs=False # Parse as scalars only
)
config.add(env_source)
# Example environment variables:
# MYAPP_SERVERS__0__HOST=server1.example.com
# MYAPP_SERVERS__0__PORT=8080
# MYAPP_SERVERS__1__HOST=server2.example.com
# MYAPP_SERVERS__1__PORT=8081
# Results in: config['servers'] = [{'host': 'server1.example.com', 'port': 8080}, ...]import confuse
import os
# Set up environment variables for list conversion
os.environ['MYAPP_ITEMS__0'] = 'first'
os.environ['MYAPP_ITEMS__1'] = 'second'
os.environ['MYAPP_ITEMS__2'] = 'third'
config = confuse.Configuration('myapp', read=False)
config.set_env('MYAPP_')
# Automatically converted to list
items = config['items'].get(list) # ['first', 'second', 'third']import confuse
import os
# Environment variable with YAML content
os.environ['MYAPP_COMPLEX'] = '''
database:
host: localhost
port: 5432
features: [auth, logging, metrics]
'''
config = confuse.Configuration('myapp', read=False)
# Parse environment values as full YAML documents
env_source = confuse.EnvSource('MYAPP_', parse_yaml_docs=True)
config.add(env_source)
# Access parsed YAML structure
db_host = config['complex']['database']['host'].as_str() # 'localhost'
features = config['complex']['features'].as_str_seq() # ['auth', 'logging', 'metrics']import confuse
import argparse
config = confuse.Configuration('myapp')
# Set up argument parser
parser = argparse.ArgumentParser()
parser.add_argument('--host', default='localhost')
parser.add_argument('--port', type=int, default=8080)
parser.add_argument('--database-url', dest='database.url')
parser.add_argument('--enable-feature', action='append', dest='features')
parser.add_argument('--verbose', action='store_true')
args = parser.parse_args()
# Overlay arguments onto configuration
config.set_args(args, dots=True)
# Command line overrides configuration files
host = config['host'].as_str() # From --host or config file
port = config['port'].as_number() # From --port or config file
db_url = config['database']['url'].as_str() # From --database-url
features = config['features'].as_str_seq() # From --enable-feature (multiple)import confuse
import yaml
# Create custom loader with additional constructors
class CustomLoader(confuse.yaml_util.Loader):
pass
def construct_include(loader, node):
"""Custom constructor for !include directive"""
filename = loader.construct_scalar(node)
with open(filename, 'r') as f:
return yaml.load(f, Loader=CustomLoader)
CustomLoader.add_constructor('!include', construct_include)
# Use custom loader
config = confuse.Configuration('myapp', read=False, loader=CustomLoader)
config.set_file('config.yaml') # Can now use !include directiveimport confuse
config = confuse.Configuration('myapp', read=False)
# Add sources in reverse priority order (lowest to highest)
config.add({'timeout': 30, 'debug': False}) # 1. Defaults (lowest)
config.set_file('/etc/myapp/system.yaml') # 2. System config
config.read(user=True, defaults=False) # 3. User config
config.set_env('MYAPP_') # 4. Environment variables
# Command-line arguments would be added with set_args() # 5. CLI args (highest)
# Runtime overrides (highest priority)
config.set({'debug': True}) # 6. Runtime (highest)
# Values are resolved in priority order
debug_mode = config['debug'].get(bool) # True (from runtime override)
timeout = config['timeout'].get(int) # 30 (from defaults, unless overridden)import confuse
config = confuse.Configuration('myapp')
# Configuration files searched in platform-specific locations:
# Linux: ~/.config/myapp/config.yaml, /etc/xdg/myapp/config.yaml, etc.
# macOS: ~/Library/Application Support/myapp/config.yaml, ~/.config/myapp/config.yaml, etc.
# Windows: %APPDATA%\\myapp\\config.yaml
print(f"Config directory: {config.config_dir()}")
print(f"User config file: {config.user_config_path()}")
# Get all platform-specific directories
config_directories = confuse.config_dirs()
print("Config search paths:", config_directories)import confuse
try:
# This may raise ConfigReadError if file is malformed
yaml_source = confuse.YamlSource('bad_config.yaml')
except confuse.ConfigReadError as e:
print(f"Configuration file error: {e}")
try:
# Load YAML string with error handling
config_data = confuse.load_yaml_string('''
invalid: [unclosed list
''', 'config string')
except confuse.ConfigReadError as e:
print(f"YAML parsing error: {e}")
# Optional file source (no error if missing)
optional_source = confuse.YamlSource('optional_config.yaml', optional=True)
config.add(optional_source)import confuse
config = confuse.Configuration('myapp')
config.set({'database': {'host': 'localhost', 'port': 5432}})
# Generate YAML representation
yaml_output = config.dump(full=True, redact=False)
print("Full configuration:")
print(yaml_output)
# Generate with sensitive values redacted
safe_output = config.dump(full=True, redact=True)
print("Safe configuration:")
print(safe_output)
# Custom YAML dumping with Confuse's Dumper
import confuse.yaml_util
data = {'servers': ['web1', 'web2'], 'enabled': True}
yaml_str = confuse.yaml_util.yaml.dump(data, Dumper=confuse.yaml_util.Dumper)
print(yaml_str) # Uses Confuse's formatting preferencesInstall with Tessl CLI
npx tessl i tessl/pypi-confuse