diff --git a/src/depnav/config.py b/src/depnav/config.py index 90d9496..3a54dc3 100644 --- a/src/depnav/config.py +++ b/src/depnav/config.py @@ -1 +1,161 @@ -/app/depnav/src/depnav/config.py \ No newline at end of file +from pathlib import Path +from typing import Any, Optional + +import yaml + + +class Config: + """Configuration management for depnav.""" + + DEFAULT_CONFIG_FILE = ".depnav.yaml" + DEFAULT_THEME = "default" + DEFAULT_EXCLUDE_PATTERNS = [ + "__pycache__", + ".git", + "node_modules", + "*.egg-info", + ".venv", + "venv", + ".tox", + "build", + "dist", + "*.pyc", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + ".hypothesis", + ] + DEFAULT_INCLUDE_EXTENSIONS = [".py", ".js", ".ts", ".go"] + + def __init__( + self, + config_file: Optional[Path] = None, + theme: str = DEFAULT_THEME, + exclude_patterns: Optional[list[str]] = None, + include_extensions: Optional[list[str]] = None, + max_nodes: int = 50, + layout: str = "tree", + ): + self.config_file = config_file + self.theme = theme + self.exclude_patterns = ( + exclude_patterns if exclude_patterns is not None else self.DEFAULT_EXCLUDE_PATTERNS.copy() + ) + self.include_extensions = ( + include_extensions if include_extensions is not None else self.DEFAULT_INCLUDE_EXTENSIONS.copy() + ) + self.max_nodes = max_nodes + self.layout = layout + + self._config: dict[str, Any] = {} + self._load_config() + + def _load_config(self) -> None: + """Load configuration from file if it exists.""" + if self.config_file and self.config_file.exists(): + try: + content = self.config_file.read_text() + + if self.config_file.suffix == ".toml": + import tomli + + data = tomli.loads(content) + data = data.get("tool", {}).get("depnav", {}) + else: + data = yaml.safe_load(content) or {} + + if isinstance(data, dict): + self._config.update(data) + except (yaml.YAMLError, OSError): + pass + + def _apply_env_overrides(self) -> None: + """Apply environment variable overrides.""" + env_map = { + "DEPNAV_CONFIG": ("config_file", None), + "DEPNAV_THEME": ("theme", "default"), + "DEPNAV_PAGER": ("pager", "auto"), + } + + for env_var, (attr, default) in env_map.items(): + value = Path.environ.get(env_var) + if value is not None: + if attr == "config_file": + setattr(self, attr, Path(value)) + else: + setattr(self, attr, value) + + def get(self, key: str, default: Any = None) -> Any: + """Get a configuration value.""" + return self._config.get(key, default) + + def set(self, key: str, value: Any) -> None: + """Set a configuration value.""" + self._config[key] = value + + def merge_config(self, config_dict: dict[str, Any]) -> None: + """Merge additional configuration.""" + if isinstance(config_dict, dict): + self._config.update(config_dict) + + def load_from_yaml(self, path: Path) -> None: + """Load configuration from a YAML file.""" + content = path.read_text() + data = yaml.safe_load(content) or {} + self.merge_config(data) + + def load_from_pyproject(self, path: Path) -> None: + """Load configuration from a pyproject.toml file.""" + if path.name == "pyproject.toml": + import tomli + + content = path.read_text() + data = tomli.loads(content) + data = data.get("tool", {}).get("depnav", {}) + self.merge_config(data) + + def get_theme(self) -> str: + """Get the current theme.""" + return self.theme or self.DEFAULT_THEME + + def default_theme(self) -> str: + """Get the default theme name.""" + return self.DEFAULT_THEME + + def get_exclude_patterns(self) -> list[str]: + """Get the exclude patterns.""" + return self.exclude_patterns or self.DEFAULT_EXCLUDE_PATTERNS + + def get_include_extensions(self) -> list[str]: + """Get the include extensions.""" + return self.include_extensions or self.DEFAULT_INCLUDE_EXTENSIONS + + def save_config(self, path: Path) -> None: + """Save configuration to a file.""" + config_data = { + "theme": self.theme, + "exclude_patterns": self.exclude_patterns, + "include_extensions": self.include_extensions, + "max_nodes": self.max_nodes, + "layout": self.layout, + } + path.write_text(yaml.dump(config_data)) + + def __repr__(self) -> str: + return ( + f"Config(theme={self.theme!r}, " + f"exclude_patterns={self.exclude_patterns!r}, " + f"include_extensions={self.include_extensions!r})" + ) + + +def load_config(config_path: Optional[Path] = None) -> Config: + """Load configuration with environment variable overrides.""" + if config_path is None: + env_config = Path.environ.get("DEPNAV_CONFIG", "") + if env_config: + config_path = Path(env_config) + else: + config_path = Path.cwd() / Config.DEFAULT_CONFIG_FILE + + return Config(config_file=config_path)