diff --git a/doc2man/config.py b/doc2man/config.py new file mode 100644 index 0000000..124d499 --- /dev/null +++ b/doc2man/config.py @@ -0,0 +1,121 @@ +"""Configuration file handling for Doc2Man.""" + +from pathlib import Path +from typing import Any, Dict, List, Optional + +import yaml + + +def find_config_file(start_path: Path = Path.cwd()) -> Optional[Path]: + """Find configuration file starting from the given path.""" + search_paths = [ + ".doc2man.yaml", + ".doc2man.yml", + "doc2man.yaml", + "doc2man.yml", + "pyproject.toml", + ] + + current = start_path + while current != current.parent: + for config_name in search_paths: + config_path = current / config_name + if config_path.exists(): + return config_path + current = current.parent + + return None + + +def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: + """Load configuration from file.""" + if config_path is None: + config_path = find_config_file() + if config_path is None: + return {} + else: + config_path = Path(config_path) + + config_path = config_path.resolve() + + if not config_path.exists(): + return {} + + if config_path.name == "pyproject.toml": + return load_pyproject_config(config_path) + else: + return load_yaml_config(config_path) + + +def load_yaml_config(config_path: Path) -> Dict[str, Any]: + """Load configuration from YAML file.""" + try: + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) or {} + return config.get("doc2man", config) + except yaml.YAMLError as e: + raise ValueError(f"Error parsing configuration file {config_path}: {e}") + except IOError as e: + raise ValueError(f"Error reading configuration file {config_path}: {e}") + + +def load_pyproject_config(config_path: Path) -> Dict[str, Any]: + """Load configuration from pyproject.toml.""" + try: + import tomli + + with open(config_path, "rb") as f: + pyproject = tomli.load(f) + + tool_section = pyproject.get("tool", {}) + doc2man_section = tool_section.get("doc2man", {}) + + return doc2man_section + + except ImportError: + import configparser + + config = configparser.ConfigParser() + config.read(config_path) + + if "doc2man" in config: + return dict(config["doc2man"]) + + return {} + + +def save_config(config: Dict[str, Any], config_path: Path) -> None: + """Save configuration to file.""" + with open(config_path, "w", encoding="utf-8") as f: + yaml.dump({"doc2man": config}, f, default_flow_style=False, indent=2) + + +def merge_configs(base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: + """Merge two configurations, with override taking precedence.""" + merged = base_config.copy() + for key, value in override_config.items(): + if isinstance(value, dict) and key in merged and isinstance(merged[key], dict): + merged[key] = merge_configs(merged[key], value) + else: + merged[key] = value + return merged + + +def validate_config(config: Dict[str, Any]) -> List[str]: + """Validate configuration and return list of warnings.""" + warnings = [] + + valid_formats = ["man", "markdown", "html"] + if "format" in config and config["format"] not in valid_formats: + warnings.append(f"Invalid format '{config['format']}', must be one of {valid_formats}") + + if "input" in config and not isinstance(config["input"], list): + warnings.append("'input' must be a list of paths") + + if "output" in config and not isinstance(config["output"], str): + warnings.append("'output' must be a string path") + + if "exclusions" in config and not isinstance(config["exclusions"], list): + warnings.append("'exclusions' must be a list of patterns") + + return warnings