Source code for varlord.sources.dotenv
"""
DotEnv source.
Loads configuration from .env files using python-dotenv.
Only loads variables that map to fields defined in the model.
"""
from __future__ import annotations
from typing import Any, Mapping, Optional, Type
try:
from dotenv import dotenv_values
except ImportError:
dotenv_values = None # type: ignore
from varlord.metadata import get_all_field_keys
from varlord.sources.base import Source, normalize_key
[docs]
class DotEnv(Source):
"""Source that loads configuration from .env files.
Requires the 'dotenv' extra: pip install varlord[dotenv]
Only loads variables that map to fields defined in the model.
Model is required and will be auto-injected by Config.
Example:
>>> @dataclass
... class Config:
... api_key: str = field() # Required by default
>>> # .env file: API_KEY=value1 OTHER_VAR=ignored
>>> source = DotEnv(".env", model=Config)
>>> source.load()
{'api_key': 'value1'} # OTHER_VAR is ignored
"""
[docs]
def __init__(
self,
dotenv_path: str = ".env",
model: Optional[Type[Any]] = None,
encoding: Optional[str] = None,
source_id: Optional[str] = None,
):
"""Initialize DotEnv source.
Args:
dotenv_path: Path to .env file
model: Model to filter .env variables.
Only variables that map to model fields will be loaded.
Model is required and will be auto-injected by Config.
encoding: File encoding (default: None, uses system default)
source_id: Optional unique identifier (default: auto-generated from path)
Raises:
ImportError: If python-dotenv is not installed
"""
if dotenv_values is None:
raise ImportError(
"python-dotenv is required for DotEnv source. "
"Install it with: pip install varlord[dotenv]"
)
# Generate ID before calling super() if not provided
if source_id is None:
source_id = f"dotenv:{dotenv_path}"
super().__init__(model=model, source_id=source_id)
self._dotenv_path = dotenv_path
self._encoding = encoding
@property
def name(self) -> str:
"""Return source name."""
return "dotenv"
def _generate_id(self) -> str:
"""Generate unique ID for DotEnv source."""
return f"dotenv:{self._dotenv_path}"
[docs]
def load(self) -> Mapping[str, Any]:
"""Load configuration from .env file, filtered by model fields.
Returns:
A mapping of normalized keys to their values.
Only includes variables that map to model fields.
Raises:
ValueError: If model is not provided
"""
# Reset status
self._load_status = "unknown"
self._load_error = None
try:
if not self._model:
raise ValueError("DotEnv source requires model (should be auto-injected by Config)")
if dotenv_values is None:
self._load_status = "failed"
self._load_error = "python-dotenv not installed"
return {}
# Check if file exists
import os
if not os.path.exists(self._dotenv_path) or not os.path.isfile(self._dotenv_path):
self._load_status = "not_found"
self._load_error = None # File not found is normal
return {}
# Load all variables from .env file
raw_values = dotenv_values(self._dotenv_path, encoding=self._encoding) or {}
# Get all valid field keys from model
valid_keys = get_all_field_keys(self._model)
# Filter by model fields
result = {}
for env_key, env_value in raw_values.items():
normalized_key = normalize_key(env_key)
if normalized_key in valid_keys:
result[normalized_key] = env_value
self._load_status = "success"
return result
except FileNotFoundError:
self._load_status = "not_found"
self._load_error = None # File not found is normal
return {}
except Exception as e:
self._load_status = "failed"
self._load_error = str(e)
if isinstance(e, ValueError):
raise
return {}
[docs]
def __repr__(self) -> str:
"""Return string representation."""
return f"<DotEnv(path={self._dotenv_path!r})>"