Source code for argclass._defaults
"""Default value parsers for loading configuration from files."""
import configparser
import json
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, Iterable, Mapping, Tuple, Union
try:
import tomllib
toml_load = tomllib.load
except ImportError:
try:
import tomli # type: ignore[import-not-found]
toml_load = tomli.load # type: ignore[assignment]
except ImportError:
toml_load = None # type: ignore[assignment]
[docs]
class AbstractDefaultsParser(ABC):
"""Abstract base class for parsing configuration files into defaults.
Subclass this to implement custom configuration file formats for
the `config_files` parameter of Parser.
"""
[docs]
def __init__(
self,
paths: Iterable[Union[str, Path]],
strict: bool = False,
):
self._paths = list(paths)
self._strict = strict
self._loaded_files: Tuple[Path, ...] = ()
@property
def loaded_files(self) -> Tuple[Path, ...]:
"""Return tuple of successfully loaded file paths."""
return self._loaded_files
def _filter_readable_paths(self) -> list[Path]:
"""Filter paths to only include readable, existing files."""
result = []
for path in self._paths:
path_obj = Path(path).expanduser().resolve()
if os.access(path_obj, os.R_OK) and path_obj.exists():
result.append(path_obj)
return result
[docs]
@abstractmethod
def parse(self) -> Mapping[str, Any]:
"""Parse configuration files and return defaults mapping.
Returns:
A mapping where keys are argument names or group names,
and values are either default values (str) or nested
mappings for groups.
"""
raise NotImplementedError()
[docs]
class INIDefaultsParser(AbstractDefaultsParser):
"""Parse INI configuration files for default values.
This is the default parser used by argclass. It uses Python's
configparser module to read INI files.
INI sections map to argument groups, and the [DEFAULT] section
contains top-level argument defaults.
"""
[docs]
def parse(self) -> Mapping[str, Any]:
parser = configparser.ConfigParser(
allow_no_value=True,
strict=self._strict,
)
filenames = self._filter_readable_paths()
loaded = parser.read(filenames)
self._loaded_files = tuple(Path(f) for f in loaded)
result: Dict[str, Any] = dict(
parser.items(parser.default_section, raw=True),
)
for section in parser.sections():
result[section] = dict(parser.items(section, raw=True))
return result
[docs]
class JSONDefaultsParser(AbstractDefaultsParser):
"""Parse JSON configuration files for default values.
The JSON structure should be a flat or nested object where:
- Top-level keys are argument names or group names
- Group values are objects with argument names as keys
"""
[docs]
def parse(self) -> Mapping[str, Any]:
result: Dict[str, Any] = {}
loaded_files = []
for path in self._filter_readable_paths():
try:
with path.open("r") as fp:
data = json.load(fp)
if isinstance(data, dict):
result.update(data)
loaded_files.append(path)
except (json.JSONDecodeError, OSError):
if self._strict:
raise
self._loaded_files = tuple(loaded_files)
return result
[docs]
class TOMLDefaultsParser(AbstractDefaultsParser):
"""Parse TOML configuration files for default values.
Uses stdlib tomllib (Python 3.11+) or tomli package as fallback.
The TOML structure should be:
- Top-level keys are argument names
- Tables (sections) map to argument groups
"""
[docs]
def parse(self) -> Mapping[str, Any]:
if toml_load is None:
raise RuntimeError(
"TOML support requires Python 3.11+ (tomllib) "
"or 'tomli' package: pip install tomli"
)
result: Dict[str, Any] = {}
loaded_files = []
for path in self._filter_readable_paths():
try:
with path.open("rb") as fp:
data = toml_load(fp)
if isinstance(data, dict):
result.update(data)
loaded_files.append(path)
except OSError:
if self._strict:
raise
self._loaded_files = tuple(loaded_files)
return result