Source code for configbuddy.core.config

# Copyright (c) 2024 Harim Kang
# SPDX-License-Identifier: MIT

import configparser
import json
from pathlib import Path
from typing import Any

import yaml

from .types import ConfigDiff, MergeConflict
from .visualizer import TreeVisualizer


[docs] class Config: """Base class for representing configuration file contents. This class provides functionality to load, save, and visualize configuration data from various file formats including YAML, JSON, and INI. """ def __init__(self, data: dict[str, Any], source: Path | None = None): self.data = data self.source = source self._visualizer = TreeVisualizer()
[docs] @classmethod def from_file(cls, path: str | Path) -> "Config": """Create a Config object from a file. Args: path: Path to the configuration file. Can be a string or Path object. Returns: A new Config instance containing the loaded configuration data. Raises: ValueError: If the file format is not supported. """ path = Path(path) if path.suffix in [".yaml", ".yml"]: with open(path) as f: return cls(yaml.safe_load(f), path) elif path.suffix == ".json": with open(path) as f: return cls(json.load(f), path) elif path.suffix == ".ini": parser = configparser.ConfigParser() parser.read(path) data = {s: dict(parser.items(s)) for s in parser.sections()} return cls(data, path) else: raise ValueError(f"Unsupported file format: {path.suffix}")
[docs] def to_dict(self) -> dict[str, Any]: """Convert configuration to a dictionary. Returns: Dictionary representation of the configuration data. """ return self.data
[docs] def save(self, path: str | Path | None = None) -> None: """Save configuration to a file. Args: path: Path where to save the configuration. If None, uses the source path. Raises: ValueError: If no path is specified and source is None. """ path = Path(path) if path else self.source if not path: raise ValueError("No save path specified") with open(path, "w") as f: if path.suffix in [".yaml", ".yml"]: yaml.dump(self.data, f) elif path.suffix == ".json": json.dump(self.data, f, indent=2) elif path.suffix == ".ini": # TODO: Implement INI format conversion logic pass
[docs] def visualize(self) -> None: """Visualize configuration in a tree structure. Displays the configuration data as a hierarchical tree in the console. """ self._visualizer.visualize(self)
[docs] def diff_with(self, config: "Config") -> ConfigDiff: """Compare this configuration with another using ConfigDiffer. A convenience method that uses ConfigDiffer to analyze differences between this configuration and another one. Args: config: Another Config object to compare with Returns: ConfigDiff object containing the differences between configurations """ from .differ import ConfigDiffer return ConfigDiffer.diff(self, config)
[docs] def __str__(self) -> str: """Convert configuration to a string representation. Returns: String representation of the configuration in a tree structure. """ return self._visualizer.to_string(self)
[docs] def merge_with(self, config: "Config", strategy: str = "deep") -> tuple["Config", list[MergeConflict]]: """Merge this configuration with another using ConfigMerger. A convenience method that uses ConfigMerger to merge this configuration with another one. Similar to diff_with, but for merging. Args: config: Another Config object to merge with strategy: Merge strategy to use ('deep' or 'shallow') 'deep' recursively merges nested dictionaries 'shallow' only merges top-level keys Returns: A tuple containing: - Config: The merged configuration object - list[MergeConflict]: List of merge conflicts detected during merging Example: >>> config1 = Config.from_file('base.yaml') >>> config2 = Config.from_file('override.yaml') >>> merged, conflicts = config1.merge_with(config2) >>> if conflicts: ... print("Merge conflicts detected:", conflicts) >>> merged.save('merged.yaml') """ from .merger import ConfigMerger return ConfigMerger.merge([self, config], strategy)
[docs] def validate(self, schema_path: str | Path) -> list[str] | None: """Validate configuration against a JSON schema. Args: schema_path: Path to the JSON schema file. Returns: List of validation errors or None if no errors are found. """ from .validator import ConfigValidator validator = ConfigValidator.from_file(schema_path) return validator.validate(self)