# 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)