Source code for duck.cli.commands.sync.config

"""
Loads and validates the [dependencies]/[development] sections of duck.toml.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path


try:
    import tomllib
except ImportError:
    import tomli as tomllib


from duck.cli.commands.sync.exceptions import ConfigNotFoundError, ConfigParseError


# Default filename Duck Sync looks for at the project root
CONFIG_FILENAME = "duck.toml"


[docs] @dataclass class DependencyGroup: """ A named collection of python and system dependencies. Args: python: Python packages installable via pip/uv. system: System level packages resolved through the OS package manager. requirements: Path to a requirements.txt, relative to duck.toml. Installed alongside the python list rather than replacing it. python_args: Extra CLI args appended to every python install command in this group, e.g. ["--no-cache-dir"]. python_package_args: Per-package extra args, keyed by package name, merged in after python_args. system_args: Extra CLI args appended to every system install command in this group, e.g. ["--no-install-recommends"]. system_package_args: Per-package extra args for system installs, keyed by the generic name written in the system list. """ python: list[str] = field(default_factory=list) system: list[str] = field(default_factory=list) requirements: str = "" python_args: list[str] = field(default_factory=list) python_package_args: dict[str, list[str]] = field(default_factory=dict) system_args: list[str] = field(default_factory=list) system_package_args: dict[str, list[str]] = field(default_factory=dict)
[docs] @dataclass class DuckSyncConfig: """ Fully parsed representation of a project's duck.toml file. Args: dependencies: Required runtime dependencies. development: Optional dependencies only needed for local development. overrides: Per package name overrides for system package names, keyed by backend name (apt, brew, dnf, pacman, choco). base_dir: Directory duck.toml lives in, used to resolve relative paths such as requirements. use_sudo: Whether system installs should be prefixed with sudo. None means auto-detect: skip sudo when already running as root. """ dependencies: DependencyGroup = field(default_factory=DependencyGroup) development: DependencyGroup = field(default_factory=DependencyGroup) overrides: dict[str, dict[str, str]] = field(default_factory=dict) base_dir: Path = field(default_factory=Path.cwd) use_sudo: bool | None = None
[docs] def find_config(start: Path | None = None) -> Path: """ Walks upward from start looking for duck.toml. Args: start: Directory to begin searching from. Defaults to the cwd. Returns: Path to the discovered duck.toml file. Raises: ConfigNotFoundError: If no duck.toml is found up to the filesystem root. """ current = (start or Path.cwd()).resolve() for directory in [current, *current.parents]: candidate = directory / CONFIG_FILENAME if candidate.is_file(): return candidate # Raise an exception raise ConfigNotFoundError(f"No {CONFIG_FILENAME} found in '{current}' or any parent directory.")
[docs] def load_config(path: Path | None = None) -> DuckSyncConfig: """ Reads and validates a duck.toml file into a DuckSyncConfig. Args: path: Explicit path to a duck.toml file. Auto discovered when omitted. Returns: The parsed configuration. Raises: ConfigParseError: If the file exists but is malformed. """ config_path = path or find_config() try: raw = tomllib.loads(config_path.read_text(encoding="utf-8")) except tomllib.TOMLDecodeError as error: raise ConfigParseError(f"Invalid TOML in '{config_path}': {error}") from error # Get raw data dependencies_raw = raw.get("dependencies", {}) development_raw = raw.get("development", {}) sync_raw = raw.get("sync", {}) return DuckSyncConfig( dependencies=parse_dependency_group(dependencies_raw), development=parse_dependency_group(development_raw), overrides=raw.get("overrides", {}), base_dir=config_path.parent, use_sudo=sync_raw.get("use_sudo"), )
[docs] def parse_dependency_group(raw: dict) -> DependencyGroup: """ Builds a DependencyGroup from a [dependencies] or [development] table. Args: raw: The raw dict for that TOML table. Returns: The parsed DependencyGroup. """ return DependencyGroup( python=list(raw.get("python", [])), system=list(raw.get("system", [])), requirements=raw.get("requirements", ""), python_args=list(raw.get("python_args", [])), python_package_args=raw.get("python_package_args", {}), system_args=list(raw.get("system_args", [])), system_package_args=raw.get("system_package_args", {}), )