196 lines
6.8 KiB
Python
196 lines
6.8 KiB
Python
"""Configuration management for gitignore-generator."""
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
class Config:
|
|
"""Configuration management class."""
|
|
|
|
DEFAULT_CONFIG_NAME = ".gitignorerc"
|
|
|
|
def __init__(
|
|
self,
|
|
custom_templates: Optional[Dict[str, str]] = None,
|
|
default_types: Optional[List[str]] = None,
|
|
exclude_patterns: Optional[List[str]] = None,
|
|
include_patterns: Optional[List[str]] = None,
|
|
) -> None:
|
|
"""Initialize configuration."""
|
|
self.custom_templates = custom_templates or {}
|
|
self.default_types = default_types or []
|
|
self.exclude_patterns = exclude_patterns or []
|
|
self.include_patterns = include_patterns or {}
|
|
|
|
@classmethod
|
|
def load(cls, config_path: str) -> "Config":
|
|
"""Load configuration from YAML file."""
|
|
import yaml
|
|
|
|
path = Path(config_path)
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
|
|
try:
|
|
with open(path, "r") as f:
|
|
data = yaml.safe_load(f)
|
|
except yaml.YAMLError as e:
|
|
raise ValueError(f"Invalid YAML in config file: {e}")
|
|
except OSError as e:
|
|
raise OSError(f"Error reading config file: {e}")
|
|
|
|
if not isinstance(data, dict):
|
|
raise ValueError("Config file must contain a YAML dictionary")
|
|
|
|
custom_templates = data.get("custom_templates", {})
|
|
default_types = data.get("default_types", [])
|
|
exclude_patterns = data.get("exclude_patterns", [])
|
|
include_patterns = data.get("include_patterns", {})
|
|
|
|
return cls(
|
|
custom_templates=custom_templates,
|
|
default_types=default_types,
|
|
exclude_patterns=exclude_patterns,
|
|
include_patterns=include_patterns,
|
|
)
|
|
|
|
@classmethod
|
|
def find_config(
|
|
cls, search_paths: Optional[List[str]] = None
|
|
) -> Optional[Path]:
|
|
"""Find configuration file in search paths."""
|
|
if search_paths is None:
|
|
search_paths = [
|
|
os.getcwd(),
|
|
os.path.expanduser("~"),
|
|
"/etc",
|
|
]
|
|
|
|
for path_str in search_paths:
|
|
path = Path(path_str)
|
|
config_path = path / cls.DEFAULT_CONFIG_NAME
|
|
if config_path.exists():
|
|
return config_path
|
|
|
|
yaml_path = path / "config.yaml"
|
|
if yaml_path.exists():
|
|
return yaml_path
|
|
|
|
return None
|
|
|
|
def save(self, config_path: str) -> None:
|
|
"""Save configuration to YAML file."""
|
|
import yaml
|
|
|
|
path = Path(config_path)
|
|
|
|
data = {
|
|
"custom_templates": self.custom_templates,
|
|
"default_types": self.default_types,
|
|
"exclude_patterns": self.exclude_patterns,
|
|
"include_patterns": self.include_patterns,
|
|
}
|
|
|
|
try:
|
|
with open(path, "w") as f:
|
|
yaml.dump(data, f, default_flow_style=False, indent=2)
|
|
except OSError as e:
|
|
raise OSError(f"Error writing config file: {e}")
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get configuration value."""
|
|
if hasattr(self, key):
|
|
return getattr(self, key)
|
|
return default
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
"""Set configuration value."""
|
|
if hasattr(self, key):
|
|
setattr(self, key, value)
|
|
|
|
def merge(self, other: "Config") -> "Config":
|
|
"""Merge with another configuration."""
|
|
return Config(
|
|
custom_templates={
|
|
**self.custom_templates,
|
|
**other.custom_templates,
|
|
},
|
|
default_types=self.default_types or other.default_types,
|
|
exclude_patterns=list(
|
|
set(self.exclude_patterns + other.exclude_patterns)
|
|
),
|
|
include_patterns={
|
|
**self.include_patterns,
|
|
**other.include_patterns,
|
|
},
|
|
)
|
|
|
|
def validate(self) -> List[str]:
|
|
"""Validate configuration and return list of errors."""
|
|
errors = []
|
|
|
|
if not isinstance(self.custom_templates, dict):
|
|
errors.append("custom_templates must be a dictionary")
|
|
else:
|
|
for key, value in self.custom_templates.items():
|
|
if not isinstance(key, str):
|
|
errors.append(f"Template key must be string: {key}")
|
|
if not isinstance(value, str):
|
|
errors.append(
|
|
f"Template value for '{key}' must be a string"
|
|
)
|
|
|
|
if not isinstance(self.default_types, list):
|
|
errors.append("default_types must be a list")
|
|
else:
|
|
for item in self.default_types:
|
|
if not isinstance(item, str):
|
|
errors.append(f"Default type must be string: {item}")
|
|
|
|
if not isinstance(self.exclude_patterns, list):
|
|
errors.append("exclude_patterns must be a list")
|
|
else:
|
|
for item in self.exclude_patterns:
|
|
if not isinstance(item, str):
|
|
errors.append(
|
|
f"Exclude pattern must be string: {item}"
|
|
)
|
|
|
|
if not isinstance(self.include_patterns, dict):
|
|
errors.append("include_patterns must be a dictionary")
|
|
else:
|
|
for key, value in self.include_patterns.items():
|
|
if not isinstance(key, str):
|
|
errors.append(f"Include pattern key must be string: {key}")
|
|
if isinstance(value, list):
|
|
for item in value:
|
|
if not isinstance(item, str):
|
|
errors.append(
|
|
f"Include pattern value for '{key}' must be string: {item}"
|
|
)
|
|
elif not isinstance(value, str):
|
|
errors.append(
|
|
f"Include pattern value for '{key}' must be string or list"
|
|
)
|
|
|
|
return errors
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert configuration to dictionary."""
|
|
return {
|
|
"custom_templates": self.custom_templates,
|
|
"default_types": self.default_types,
|
|
"exclude_patterns": self.exclude_patterns,
|
|
"include_patterns": self.include_patterns,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "Config":
|
|
"""Create configuration from dictionary."""
|
|
return cls(
|
|
custom_templates=data.get("custom_templates", {}),
|
|
default_types=data.get("default_types", []),
|
|
exclude_patterns=data.get("exclude_patterns", []),
|
|
include_patterns=data.get("include_patterns", {}),
|
|
)
|