Source code for symfluence.core.config.coercion
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2024-2026 SYMFLUENCE Team <dev@symfluence.org>
"""Centralized config coercion to replace 60+ duplicate patterns.
This module provides a single, well-tested function for converting dict configs
to SymfluenceConfig instances. All modules should use ensure_config() instead
of duplicating the lazy import + isinstance check pattern.
Environment Variables:
SYMFLUENCE_STRICT_CONFIG: If set to 'true', '1', or 'yes', coerce_config()
will raise errors instead of falling back to dict.
"""
import os
import warnings
from typing import TYPE_CHECKING, Any, Dict, Union
if TYPE_CHECKING:
from symfluence.core.config.models import SymfluenceConfig
def _is_strict_mode() -> bool:
"""Check if strict config mode is enabled via environment variable."""
value = os.environ.get('SYMFLUENCE_STRICT_CONFIG', '').lower()
return value in ('true', '1', 'yes')
[docs]
def ensure_config(config: Union[Dict[str, Any], 'SymfluenceConfig']) -> 'SymfluenceConfig':
"""
Convert dict to SymfluenceConfig if needed.
This function centralizes the config coercion pattern that was previously
duplicated across 60+ files. It uses a runtime import to avoid circular
dependency issues while providing type safety via TYPE_CHECKING.
Args:
config: Configuration as dict or SymfluenceConfig instance
Returns:
SymfluenceConfig instance
Raises:
TypeError: If config is neither a dict nor SymfluenceConfig
Example:
>>> from symfluence.core.config import ensure_config
>>> cfg = ensure_config({'DOMAIN_NAME': 'test_domain', ...})
>>> isinstance(cfg, SymfluenceConfig)
True
"""
# Runtime import to avoid circular dependency at module level
from symfluence.core.config.models import SymfluenceConfig
if isinstance(config, SymfluenceConfig):
return config
elif isinstance(config, dict):
return SymfluenceConfig(**config)
raise TypeError(
f"config must be SymfluenceConfig or dict, got {type(config).__name__}. "
"Use SymfluenceConfig.from_file() to load configuration."
)
[docs]
def coerce_config(
config: Union[Dict[str, Any], 'SymfluenceConfig'],
strict: bool = None,
warn: bool = True
) -> Union['SymfluenceConfig', Dict[str, Any]]:
"""
Convert dict to SymfluenceConfig if possible, with configurable fallback.
Similar to ensure_config but can return the original dict if conversion fails
(e.g., for partial configs in tests). This maintains backward compatibility
with code that may pass incomplete configuration dictionaries.
Args:
config: Configuration as dict or SymfluenceConfig instance
strict: If True, raise error instead of falling back to dict.
If None, uses SYMFLUENCE_STRICT_CONFIG environment variable.
warn: If True (default), emit DeprecationWarning when falling back to dict.
Set to False for tests or cases where dict fallback is intentional.
Returns:
SymfluenceConfig if conversion succeeds, original dict otherwise
(unless strict=True, which raises on failure)
Raises:
TypeError: If strict=True and config cannot be converted
ValueError: If strict=True and config validation fails
Example:
>>> # Full config converts to SymfluenceConfig
>>> cfg = coerce_config({'DOMAIN_NAME': 'test', ...})
>>>
>>> # Partial config in tests falls back to dict (with warning)
>>> partial = coerce_config({'some_key': 'value'})
>>> isinstance(partial, dict)
True
>>>
>>> # Strict mode raises instead of falling back
>>> coerce_config({'invalid': 'config'}, strict=True) # Raises!
Note:
The fallback behavior is deprecated and will be removed in a future
version. Use ensure_config() for strict conversion, or pass
warn=False if you intentionally need dict fallback (e.g., in tests).
"""
# Determine strict mode
if strict is None:
strict = _is_strict_mode()
# Runtime import to avoid circular dependency at module level
from symfluence.core.config.models import SymfluenceConfig
if isinstance(config, SymfluenceConfig):
return config
elif isinstance(config, dict):
try:
return SymfluenceConfig(**config)
except (TypeError, ValueError) as e:
if strict:
raise type(e)(
f"Config coercion failed (strict mode): {e}. "
"Ensure all required configuration fields are provided."
) from e
# Emit deprecation warning for fallback behavior
if warn:
warnings.warn(
f"coerce_config() fell back to dict due to validation error: {e}. "
"This fallback behavior is deprecated. Use ensure_config() for "
"strict conversion, or fix the configuration to include all "
"required fields. Set SYMFLUENCE_STRICT_CONFIG=true to enforce "
"strict mode globally.",
DeprecationWarning,
stacklevel=2
)
return config
return config